Flask, sessiot ja autentikointi

Harjoitellaan Flask-sovelluskehyksen ja sessioiden käyttöä WWW-sovelluksen tekemisessä. Toteutaan sessioilla yksinkertainen lomake, joka laskee lukuja yhteen.

mallivastaus ( lähdekoodi oma.py, lähdekoodi flask.py, kirjaudu, laskuri, lomake )

Hello World Flask-sovelluksena

  1. Tutustu Flaskia käsittelevään materiaaliin ja avaa avuksesi seuraavat sivut:

    Flask on rakennettu Werkzeug-WSGI-kirjaston päälle joten osa tarpeellisesta dokumentaatiosta on suoraan Werkzeugin dokumentaatiossa.

  2. Luo W-aseman cgi-bin-hakemistoon uusi alihakemisto ohjaus3
  3. Luo kansioon uusi tiedosto flask.cgi ja kirjoita siihen seuraava ohjelmakoodi:
    #!/usr/bin/python
    # -*- coding: utf-8 -*-
    
    import sys
    from wsgiref.handlers import CGIHandler
    from werkzeug.debug import DebuggedApplication
    try:
      # oma on oman python-tiedoston nimi. esim. tässä tapauksessa oma.py
      # tiedostossa on oltava Flask-sovellus
      from oma import app as application
    except:
      print "Content-Type: text/plain;charset=UTF-8\n"
      print "Syntaksivirhe:\n"
      for err in sys.exc_info():
            print err
      
    if __name__ == '__main__':
         handler = CGIHandler()
         handler.run(DebuggedApplication(application))
  4. Luo samaan kansioon myös oma.py-tiedosto ja kirjoita siihen seuraava ohjelmakoodi:
    #!/usr/bin/python
    # -*- coding: utf-8 -*-
    
    from flask import Flask, session, redirect, url_for, escape, request, Response, render_template
    import os
    app = Flask(__name__)
    
    # @app.route määrää mille osoitteille tämä funktio suoritetaan
    @app.route('/')
    def hello_world():
        return Response("Hello World", content_type="text/plain; charset=UTF-8")
        # yleensä riittää:
        # return "Hello World"
        
    if __name__ == '__main__':
        app.debug = True
        app.run(debug=True)
  5. Varmista, että tiedostojen ja kansioiden oikeudet ovat samat kuin aiemmin tehdyissä cgi-ohjelmissa.
  6. Kokeile sovellustasi osoitteessa http://users.jyu.fi/~omatunnus/cgi-bin/ohjaus3/flask.cgi/. Viimeinen /-merkki on olennainen!
  7. Jos sovelluksesi ei toimi niin lue ohjetta eteenpäin ja kokeile omalla / mikroluokan koneelle asennetulla Flaskilla
  8. WWW-palvelin suorittaa Flask-kehyksen ja oma.py-tiedostosta löytyy hello_world-funktio, joka on määrätty suoritettavaksi kun pyydetään sovelluksen juurikansiota (/). HTTP-protokollaan liittyviä asioita ei tarvitse itse tehdä vaan Flask hoitaa ne. Riittää, että funktio palauttaa merkkijonon jossa on sivulle haluttu sisältö.

Flask omalle koneelle

Asenna Anaconda (Python 2.7), jonka mukana tulee myös Flask. Anaconda on jo valmiiksi asennettuna Agoran mikroluokkiin.

Tämän jälkeen riittää, kun käynnistää oman sovelluksen Windowsin CMD-promptissa komennolla

python oma.py

ja voi kokeilla selaimella osoitteessa:

http://127.0.0.1:5000/
tai
http://localhost:5000/

Nyt saa myös selkeät virheilmoitukset suoraan selaimeen, koska Flaskin debug-tila toimii omalla koneella.

Halavassa / jalavassa tämä ei onnistu, koska Flaskia ei niistä löydy. Yliopiston mikroluokkien koneisiin on osaan asennettuna Anaconda.

Lopullinen sovellus pitää yleensä saada toimimaan muualla kuin omalla koneella ja toiminnallisuudessa voi olla eroja esim. tiedostopolut voivat olla erilaisia. Debuggaus on kuitenkin huomattavasti helpompaa omalla koneella. Jos on W:-asema verkkolevynä niin voi suoraan sieltä ajaa sovellustaan oman koneen Flaskilla ja testailla samaan aikaan myös users.jyu.fin kautta.

Huomaa, että samat absoluuttiset tiedostopolut eivät toimi lokaalisti suoritettuina ja users.jyu.fi-palvelimen kautta suoritettuna. Voit try..exceptin avulla asettaa eri polut. Kannattaa käyttää suhteellisia polkuja niin ne toimivat aina.

Debuggaus

Sitten kun flask-sovellus lähtee toimimaan niin virheiden debuggaus on haastavaa, koska users.jyu.fi:ssä ei välttämättä toimi flaskin oma debug-tila. Ohjelman kaatuessa syntaksivirheeseen tai ajonaikaiseen virheeseen on tuloksena helposti vain internal server error. Cgitb-kirjasto ei auta.

Täytyy itse logata virheet. Seuraava tekee lokitiedoston kansioon '../piilo/'. Korjaa polku omaan kansiorakenteeseesi sopivaksi:

import logging
logging.basicConfig(filename=os.path.abspath('../piilo/flask.log'),level=logging.DEBUG)
logging.debug("Tänne mitä haluaa lokiin kirjoittaa")

Varmista, että lokitiedostoon on kaikilla kirjoitusoikeus. Kts. ohjaus 1.

Älä kirjoita lokia cgi-bin-kansioon tai sen alikansioihin.. Älä myöskään kirjoita lokia w:\hidden-kansioon, koska se on users.jyu.fi-palvelimen asetuksissa määritelty erikoiskansioksi. Jostain syystä sinne kirjoittaminen ei toimi users.jyu.fi:ssä.

Älä luo flask-sovelluksen käyttämiä kansioita Windowsin kautta tai esim. WinSCP:llä vaan vain ja ainoastaan Unix-komentoriviltä. Valitettavasti users.jyu.fissä käytetty SELinux sotkee kansioiden ja tiedostojen oikeuksia ja tiedostokontekstiä jos ne tehdään Windows-levyjaon kautta.

Jotta lokista olisi iloa on käytettävä try..exceptiä. Nappaa exceptillä kiinni mahdolliset virheet ja dumppaa virheilmoitus lokiin:

try:
    #tähän virheellistä koodia
except Exception as e:
    logging.debug("Tapahtui virhe. Tähän järkevä virheilmoitus")
    # logataan myös varsinainen virheilmoitus:
    logging.debug(str(e))

Lokin sisältöä voi seurailla helpoiten ottamalla pääteyhteyden halavaan ja käyttämällä tail-komentoa:

tail -f flask.log

Tämä kyttää koko ajan tiedoston sisältöä ja näyttää heti sinne tulevat uudet rivit.

Flask ja templatet

Luodaan seuraavan kuvan esittämä lomake käyttäen jinja-templatea. Kopioi itsellesi valmis pohja ja tallenna se ohjaus3/templates-kansioon laskuri.html-nimellä.

Laskuri

  1. Lisää oma.py-tiedoston alkuun rivi:
    from flask import render_template
  2. Nyt voit käyttää Jinja-templatea aivan samaan tapaan kuin aiemmin jo opit. Kokeile muuttaa hello_world-funktiota:
       return render_template('laskuri.html')
    
  3. Muuta lomakkeen käsittelijäksi sivu itse. Vaihda templateen seuraava rivi:
    <form action="{{request.base_url}}" method="get">
    

    Template saa automaattisesti käyttöönsä Flaskin request-, session- ja g-objektit. Näiden lisäksi tulevat tietysti objektit, jotka annat render_template-kutsussa.

Jinjan templatet ja perintä

Jinjan templateilla on helppo tehdä pohjatemplate, joka määrittää kaikille sivuille yhteisen rungon. Lue Template Inheritance. Kokeile tehdä seuraavat templatet.

layout.html, joka toimii kaikkien sivujen yleispohjana

<!doctype html>
<html>
  <head>
    <link rel="stylesheet" href="filename='http://appro.mit.jyu.fi/sovellukset/demot/demo3/lomake.css') }}">
    <title>{% block title %}{% endblock %} - Mallisivu</title>
  </head>
  <body>
    <div id="content">{% block content %}{% endblock %}</div>
    <div id="footer">
      {% block footer %}
      TIEA2080
      {% endblock %}
    </div>
  </body>
</html>

Muuta laskuri.html seuraavanlaiseksi eli käytä edellä luotua layout.html-templatea ja muuta sen sisältöä

{% extends "layout.html" %}
{% block title %}Laskuri{% endblock %}
{% block content %}
<h1>Laskuri</h1>
<p>Summa on </p>
<form action="" method="POST">
<p>
<label>Anna luku<input type="text" name="luku" /></label>
</p>
<p>
<input type="submit" name="laheta" value="Laske" />
</p>
</form>{% endblock %}

Kokeile miltä sivu näyttää

Laskuri sessioilla

Laskuri

  1. Kokeile edellä luomaasi lomaketta. Toimiiko vai saatko virheilmoituksen? Kokeile vaihtaa lomakkeen lähetysmetodia POST- ja GET-metodien välillä.
  2. Hyväksytyt metodit pitää erikseen luetella:
    # method-parametrissa luetellaan metodit joita tämä funktio kelpuuttaa. 
    @app.route('/laskuri', methods=['POST','GET']) 
    def laskuri():
        return render_template('laskuri.html')
    
  3. Varsinaisessa laskurissa tarvitset request-objektin form-, args- tai values -ominaisuutta. Vrt. CGI-ohjelmassa käytetty cgi.fieldstorage.
    • request.form sisältää POST-metodilla lähetetyt tiedot
    • request.args sisältää query stringin sisältämät tiedot eli GET-metodilla lähetetyt tiedot
    • request.values sisältää molempien edellämainittujen sisällöt
    esim. request.form['nimi'] palauttaa post-metodilla tulleen nimi-kentän arvon. Jos kyseistä kenttää ei ole määritelty niin tuloksena on KeyError-poikkeus. Tämä taas aiheuttaa sivulle Bad Request -virheen. Käytä siis aina try...except-rakennetta!

    Kts. myös Werkzeug BaseRequest

    Werkzeug dekoodaa valmiiksi lomakkeilta tulevat tiedot joten itse ei tarvitse erikseen kutsua decode-funktiota.

    Toinen vaihtoehto on käyttää get- ja getlist-metodeja samaan tapaan kuin cgi.fieldstoragen kanssa. Kts. Werkzeug datastructures.

    avain = request.form.get("avain", "oletusarvo")
    avain = request.form.getlist("avain")
    
  4. Muuta ohjelmaa siten, että tulostat ennen lomaketta näkyville mahdollisesti syötetyn luvun:
    @app.route('/laskuri', methods=['POST','GET']) 
    def laskuri():
        try:
            luku = request.form['luku']
        except:
            luku = 0
        # sama lyhyemmin. voidaan antaa myös oletusarvo:
        luku = request.form.get("luku", 0)
        # jos lukuja olisi useampi niin pitää käyttää getlist-metodia
        # luvut = request.form.getlist("luku")
        return render_template('laskuri.html', luku=luku)
    
  5. Muutetaan ohjelmaa siten, että voidaan laskea yhteen syötettyjä lukuja. Ohjelman pitää siis muistaa aiemmin saatu summa. Tämä onnistuu session avulla. Sessioon tallennetut muuttujat säilyttävät arvonsa koko istunnon ajan. Lisää ohjelmakooditiedostosi alkuun rivi:
    from flask import Flask, session, redirect, url_for, escape, request, Response, render_template
    Tämän lisäksi tarvit salaisen avaimen jota käytetään sessioon liittyvän evästeen käsittelyssä. Lisää seuraava rivi app-objektin määrittelyn jälkeen:
    app.secret_key = '"\xf9$T\x88\xefT8[\xf1\xc4Y-r@\t\xec!5d\xf9\xcc\xa2\xaa'

    Sinun pitäisi muodostaa itse oma salainen avain. Se onnistuu esimerkiksi käynnistämällä python-tulkki komentoriviltä ja antamalla komennot:

    import os
    os.urandom(24)
    
  6. Luo uusi sessiomuuttuja:
    session['summa'] = 10
    sessiomuuttujia käytetään samaan tapaan kuin dictejä
  7. sessio-muuttujien pitäisi näkyä suoraan templateen. Kokeile tulostaa session['summa']-muuttujan arvo www-sivulle
  8. Yritä kasvattaa sessiomuuttujasi arvoa ohjelman jokaisella suorituskerralla.

    Ensimmäisellä käyttökerralla sessio-muuttujaa ei ole määritelty joten varminta on testata pythonin poikkeuskäsittelyn (try...except) avulla onko sessiomuuttujalla jo arvo vai ei. Jos muuttuja on alustamaton niin yritys lisätä muuttujan arvoa aiheuttaa poikkeuksen jolloin voidaan tehdä muuttujan alustaminen.

  9. Ota lomakkeelle annettu luku talteen. Tarkista, että lomakkeelle on varmasti syötetty kokonaisluku. Lisää luku sessiomuuttujaan. Tulosta yhteenlaskettu summa sivulle.
  10. Istunnon ja siihen liittyvien muuttujien ylläpitäminen edellyttää, että käytetty selain tukee evästeitä. Evästeessä kuljetetaan selaimen ja WWW-palvelimen välillä istuntoon liittyvää tunnistetta. Katso Firefoxin Tools|Web developer|Network-välilehdeltä minkälaisia HTTP-otsakkeita liikkuu selaimesi ja WWW-palvelimen välillä. Sieltä pitäisi löytyä Set-Cookie-alkuisia rivej. Kokeile myös Web Developer Toolbarin Cookies|View Cookie Information-valinnan avulla millaisen evästeen flask asettaa käyttäessäsi sessioita.

    Seuraavassa kuvassa hahmotetaan sessioon liittyvää toimintaa PHP-sovelluksessa. Toiminta on täysin vastaavaa flask-sovelluksilla.

    Sessiot ja client-server

    Testauksessa kannattaa käyttää myös Web Developer Toolbarista löytyvää valintaa Cookies | Delete Domain Cookies. Tämä poistaa näkyvillä olevaan sivuun liittyvät evästeet. Poistamalla evästeet palaa laskuri alkutilaan.

  11. Tee uusi sivu joka poistaa laskurisi session.pop('summa',None)-metodilla. Kokeile nollautuuko laskuri.
    @app.route('/nollaa') 
    def nollaa():
        session.pop('summa',None)
        # url_for-metodilla voidaan muodostaa osoite haluttuun funktioon. redirect taas ohjaa suoraan tälle sivulle joten 
        # nollaa-osoite ei tarvitse omaa sisältöä    
        return redirect(url_for('laskuri'))
    

Autentikointi

Käyttäjän kirjautuminen ja autentikointi järjestelmään tehdään sessioiden avulla seuraavalla tavalla:

  1. Täytyy ensimmäisenä luoda sivu jolla voi kirjautua sisään. Tee uusi sivu (/kirjaudu) ja sijoita sivulle kirjautumislomake jolla kysytään tunnus ja salasana

    autentikointilomake

  2. Tarkista sivulla, että käyttäjätunnus on syötetty ja salasanaksi on syötetty "testi". Salasanan vertailu on tehtävä seuraavalla tavalla:
        import hashlib
        m = hashlib.sha512()
        avain = u"omasalainenavain"
        m.update(avain.encode("UTF-8"))
        m.update(salasana.encode("UTF-8"))
        if len(tunnus) and m.hexdigest() == "366e90b5fe29a9d9c1420afa334c4b19c4d63dcd200f424b7a9fe3328a352da5818fc03cffa463c2362db3535b612df4eb27df33d4720fbf592964571ad7572e":
            # jos kaikki ok niin asetetaan sessioon tieto kirjautumisesta ja ohjataan laskurisivulle
            session['kirjautunut'] = "ok"
            return redirect(url_for('laskuri'))
        # jos ei ollut oikea salasana niin pysytään kirjautumissivulla. 
        return render_template('kirjaudu.html')
    

    Selkokielistä salasanaa ei siis vertailla vaan salasanasta jollakin algoritmilla laskettua merkkijonoa. Testi-salasanaa vastaava merkkijono on tuotettu seuraavalla tavalla. Oikean salasanan vertailu pitää tehdä samalla tavalla jolloin voi verrata onko m.hexdigest-funktion palauttama merkkijono sama kuin haluttu.

    >>> import hashlib
    >>> m = hashlib.sha512()
    >>> m.update(u"omasalainenavain".encode("UTF-8"))
    >>> m.update(u"testi".encode("UTF-8"))
    >>> m.hexdigest()
    "366e90b5fe29a9d9c1420afa334c4b19c4d63dcd200f424b7a9fe3328a352da5818fc03cffa463c2362db3535b612df4eb27df33d4720fbf592964571ad7572e"
  3. Kokeile toimiiko kirjautuminen eli ohjaudutko laskurisivulle jos syötät oikean salasanan
  4. Tee myös logout-sivu, joka poistaa sessiosta kirjautunut-avaimen ja ohjaa sen jälkeen kirjaudu-sivulle
  5. Vielä tarvitaan oma decorator, jolla saadaan helposti yhdistetty kirjautumisvaatimus halutuille sivuille. Vrt. app.routes. Lisää sivun alkuun:
    from functools import wraps
    Kirjoita seuraavanlainen funktio:
    def auth(f):
        ''' Tämä decorator hoitaa kirjautumisen tarkistamisen ja ohjaa tarvittaessa kirjautumissivulle
        '''
        @wraps(f)
        def decorated(*args, **kwargs):
            # tässä voisi olla monimutkaisempiakin tarkistuksia mutta yleensä tämä riittää
            if not 'kirjautunut' in session:
                return redirect(url_for('kirjaudu'))
            return f(*args, **kwargs)
        return decorated
    
  6. Kokeile nyt käyttää omaa @auth-decoratoria. Lisää se esim. laskuri-funktion eteen:
    @app.route('/laskuri', methods=['POST','GET']) 
    @auth
    def laskuri():
    
  7. Kokeile mennä selaimella laskurisivulle. Ohjaudutko kirjautumiseen? Jos et niin muista poistaa vanha kirjautuminen logout-sivulla ja kokeile sen jälkeen uudelleen.

Tietojen validointi

Arvosanalaskuri

Luo edellä olevaa kuvaa vastaava Flask-sivu. Voit käyttää valmista pohjaa.

Lisää tarkistus jossa varmistetaan, että etunimi ja sukunimi on täytetty ja tiedekunta on valittu. Tee tarkistus vain jos lomake on täytetty eli tarkistusta ei pidä tehdä sivulle ensimmäistä kertaa tultaessa.

  1. Lisää virheilmoitukset yhteen virhesanakirjaan, jossa avaimena käytetään virheellisen kentän nimeä ja arvona virheilmoitustekstiä.
  2.     # luodaan dict
        kentat = {"etunimi":"","sukunimi":"","tdk":""}
        errors = dict(kentat) #tekee kopion samoilla avaimilla
        if request.method == 'POST': #varmistetaan, että lomake on lähetetty
            for k in errors:
                try:
                        kentat[k] = request.form[k]
                except KeyError:
                        errors[k] = "!"
    
  3. Muodosta python-sanakirja (dict) tiedekunnista ja vie se templatelle ja luo sen perusteella näkymässä olevan alasvetovalikon sisältö. Käytä avaimena tiedekunnan numeroa.
        tiedekunnat = {0:"Valitse tiedekunta", 1:"Humanistinen tiedekunta",2:"Informaatioteknologian tiedekunta", 3:"Kasvatustieteiden tiedekunta", 4:"Liikunta- ja terveystieteiden tiedekunta", 5:"Matemaattis-luonnontieteellinen tiedekunta", 6:"Taloustieteiden tiedekunta", 7:"Yhteiskuntatieteellinen tiedekunta"}
    

    Huomaa, että dictissä olevat avaimet ovat kokonaislukuja ja lomakkeelta saadut tiedot ovat aina merkkijonoja.

  4. Jos tiedoissa oli virheitä, niin tulosta virhe-ilmoitus seuraavilla tavoilla:
    • Ensimmäinen versio: Jos virheitä on niin tulosta yleinen virheilmoitus.
    • Toinen versio: Käy läpi kaikki virhetaulukon virheet ja tulosta ne lomakkeen alussa.
    • Kolmas versio: Tulosta kunkin lomake-elementin viereen siihen liittyvä virheilmoitus.
  5. Aseta kuhunkin lomakkeen kenttään oletusarvoksi edellisellä kerralla syötetty teksti tai valinta
  6. Tekstikentissä, joihin pitäisi syöttää viikkotehtävien pisteet, ei ole vielä name-ominaisuutta asetettu. Mitä name-ominaisuuksiin pitäisi laittaa, että saat kaikki viikkotehtäväpisteet varmasti oikein ja kätevästi käsiteltäväksesi? Huomaathan, että selain voi lähettää lomakkeen kenttien arvot satunnaisessa järjestyksessä.
  7. Tarkista, että viikkotehtäviin on laitettu numero. Tarkista, että numero on väliltä 0-5. Älä kuitenkaan herjaa tyhjästä tekstilaatikosta. Kokeile tehdä tarkistus siten, että käyt yhdessä silmukassa läpi kaikki viikkotehtävien syötteet. Aseta virheellisten pistekenttien taustaväriksi punainen.

Käyttäjien kommentit

Kommentoi Lisää kommentti
Kurssimateriaalien käyttäminen kaupallisiin tarkoituksiin tai opetusmateriaalina ilman lupaa on ehdottomasti kielletty!
http://appro.mit.jyu.fi/tiea2080/ohjaus/ohjaus3/
© Tommi Lahtonen (tommi.j.lahtonen@jyu.fi) <http://hazor.iki.fi/>
2018-03-01 10:59:53
Informaatioteknologia - Jyväskylän yliopiston informaatioteknologian tiedekunta