Alkalmazások fejlesztése

5. előadás

Horváth Győző
Egyetemi adjunktus
1117 Budapest, Pázmány Péter sétány 1/c., 2.420-as szoba
Tel: (1) 372-2500/1816
horvath.gyozo@inf.elte.hu

Eddig

  • JavaScript nyelv
  • Alkalmazás architektúrák (MVC)
  • HTTP, Express
  • Statikus fájlok kiszolgálása
  • Dinamikus tartalom megjelenítése
  • Űrlapfeldolgozás
  • Adattárolás

Ezen a héten

  • Absztrakt modellréteg (ORM)
  • Kód szervezése
  • Hitelesítés és jogosultságkezelés

Kód szervezése

Cél

Eddig: egyetlen server.js fájlba dolgoztunk

Cél: a különböző funkciójú kódrészletek rendszerezett elkülönítése!

Szervezőelv

MVC mintának megfelelően

  • models
  • views (már van :) )
  • controllers (másik neve lehetne routes)

Egyéb lehetőségek

  • config: konfigurációs állományok
  • viewmodels (ha már bevezettük)

Megvalósítás: Node.js modulrendszer (ismétlés)

  • egy modul = egy fájl
  • require: importálás
  • module.exports: exportálás
//Modul: math.js
function add(a, b) {
    return a + b;
}

module.exports = add;
//Főprogram: index.js
var add = require('./math');

console.log(add(2, 3));

Feladat

A fenti elgondolásnak megfelelően bontsuk részekre az alkalmazásunkat!

1. lépés

Waterline konfiguráció kiemelése

// config/waterline.js
module.exports = {
    adapters: {
        /* ... */
    },
    connections: {
        /* ... */
    },
    defaults: {
        /* ... */
    },
};
// server.js
var waterlineConfig = require('./config/waterline');

orm.initialize(waterlineConfig, function(err, readyModels) {
    /* ... */
});

2. lépés

Modellek kiemelése

// models/error.js
module.exports = {
    identity: 'error',
    /* ... */
};
// server.js
var errorCollection = require('./models/error');

orm.loadCollection(Waterline.Collection.extend(errorCollection));

3. lépés

Vezérlők kiemelése

// controllers/error.js
var express = require('express');

var router = express.Router();

router.get('/list', function (req, res) { /* ... */ });
router.get('/new',  function (req, res) { /* ... */ });
router.post('/new', function (req, res) { /* ... */ });

module.exports = router;
// server.js
var errorRouter = require('./routes/error');

app.use('/errors', errorRouter);

4. lépés

Nézetmodellek

// viewmodels/error.js
module.exports = {
    decorateErrors: function decorateErrors(errorContainer) {
        /* ... */
    }
}

Otthoni feladat

Feladat

Próbáld meg az Express applikációt (app változó) és az ORM részt (orm változó) is külön fájlba szervezni!

Hitelesítés és jogosultságkezelés

Hitelesítés

Ki használja az alkalmazást?

Ismerem?

Eredmény: vendég/azonosított felhasználó

Általános megoldás

Sikeres azonosítás után a felhasználó munkamenetébe egy speciális tokent helyezünk el.

Ennek megléte vagy hiánya jelzi az azonosítás állapotát.

Lépések:

  • beléptető űrlap (login form)
  • sikeres belépés esetén --> token elhelyezése sütiben
  • minden kérésnél: ha ez a token megvan (sütik között), akkor azonosított
  • ettől függően más logika, más nézet lehet

Passport.js

  • Dokumentáció
  • Egyszerű interfész
  • Hitelesítési stratégiák
    • Lokális
    • Facebook
    • Google
    • Twitter
    • Github

Telepítés és használat

npm install passport --save
var passport = require('passport');

Stratégia beállítása

Telepítés

npm install passport-<strategy> --save

Használat

var Strategy = require('passport-<strategy>');

passport.use([name, ] new Strategy([options, ]
    function(username, password, done) {
        //Felhasználónév és jelszó ellenőrzése

        // Eredmény jelzése
        return done(err); //Hiba történt
        return done(null, false, { message: 'Üzenet' }); //Sikertelen azonosítás
        return done(null, user); //Sikeres azonosítás, user objektum visszaadása
    }
));

Middleware-ek regisztrációja

//Passport middlewares
app.use(passport.initialize());

//Session esetén (opcionális)
app.use(passport.session());

Munkamenet beállítása

Mit tároljunk a munkamenetben?

// Sorosítás a munkamenetbe
passport.serializeUser(function(user, done) {
    done(null, <munkamenetben tárolandó adat>);
});

// Visszaállítás a munkamenetből
passport.deserializeUser(function(obj, done) {
    done(null, <az elvárt user objektum>);
});

Hitelesítés használata

passport.authenticate() metódus a végpontoknál

// Átirányítási paraméterekkel
app.post('/signup', passport.authenticate(<strategy>, {
    successRedirect:    '/login',
    failureRedirect:    '/login/signup',
    failureFlash:       true, // req.flash() metódusra van szükség!
    failureMessage:     "Hibás adatok", // opcionális alapértelmezett üzenet
    badRequestMessage:  'Hiányzó adatok', // hiányzó hitelesítési adatok hibaüzenete
}));

// Callback függvénnyel
app.post('/login', passport.authenticate(<strategy>),
    function(req, res) {
        // Sikeres hitelesítésnél hívódik meg
        // `req.user` a hitelesített user
        res.redirect('/users/' + req.user.username);
    });

Kijelentkezés

req.logout()

Autorizáció

Jogosultságkezelés

Az azonosított felhasználó milyen erőforrásokhoz fér hozzá?

Többszintű lehet:

  • alkalmazásszintű
  • végpont
  • oldal része
  • mezőszintű

Felhasználók csoportosítása: szerepkörök

Megoldások

Tipikusan végponti middleware-ként épülnek be.

  • Passport
    • req.isAuthenticated()
  • Modul
    • connect-ensure-login
  • Magas szintű
    • acl

Feladat

A hibák kezelése (listázás, új felvétele) csak azonosított személyeknek legyen elérhető. Ehhez először tegyük lehetővé felhasználók regisztrálását és bejelentkeztetését, kijelentkeztetését, majd a szóban forgó végpontok védelmét.

1. lépés

Login oldal sablonja (views/login/index.hbs)

<div class="page-header">
    <h1>Bejelentkezés</h1>
</div>

<form class="form-horizontal" method="post">
    <fieldset>
        <div class="form-group">
            <label for="neptun" class="col-lg-2 control-label">Neptun</label>
            <div class="col-lg-10">
                <input class="form-control" id="neptun" name="neptun" type="text" value="{{data.neptun}}">
            </div>
        </div>
        <div class="form-group">
            <label for="password" class="col-lg-2 control-label">Jelszó</label>
            <div class="col-lg-10">
                <input class="form-control" type="password" id="password" name="password">
            </div>
        </div>
        <div class="form-group">
            <div class="col-lg-10 col-lg-offset-2">
                <button type="reset" class="btn btn-default">Cancel</button>
                <button type="submit" class="btn btn-primary">Submit</button>
            </div>
        </div>
    </fieldset>
</form>

<div class="list-group">
  <a href="/login/signup" class="list-group-item">
    Regisztráció
  </a>
</div>

2. lépés

/login végpontra bekötése (controllers/login.js)

router.get('/', function (req, res) {
    res.render('login/index');
})

3. lépés

A regisztrációs oldal sablonja (views/index/signup)

<div class="page-header">
    <h1>Regisztráció</h1>
</div>

<form class="form-horizontal" method="post">
    <fieldset>
        <div class="form-group">
            <label for="surname" class="col-lg-2 control-label">Vezetéknév</label>
            <div class="col-lg-10">
                <input class="form-control" type="text" id="surname" name="surname">
            </div>
        </div>
        <div class="form-group">
            <label for="forename" class="col-lg-2 control-label">Keresztnév</label>
            <div class="col-lg-10">
                <input class="form-control" type="text" id="forename" name="forename">
            </div>
        </div>
        <div class="form-group">
            <label for="neptun" class="col-lg-2 control-label">Neptun-kód</label>
            <div class="col-lg-10">
                <input class="form-control" type="text" id="neptun" name="neptun">
            </div>
        </div>
        <div class="form-group">
            <label for="password" class="col-lg-2 control-label">Jelszó</label>
            <div class="col-lg-10">
                <input class="form-control" type="password" id="password" name="password">
            </div>
        </div>
        <div class="form-group">
            <label for="avatar" class="col-lg-2 control-label">Avatar URL</label>
            <div class="col-lg-10">
                <input class="form-control" type="text" id="avatar" name="avatar">
            </div>
        </div>
        
        <div class="form-group">
            <div class="col-lg-10 col-lg-offset-2">
                <button type="reset" class="btn btn-default">Cancel</button>
                <button type="submit" class="btn btn-primary">Submit</button>
            </div>
        </div>
    </fieldset>
</form>

4. lépés

/login/signup végpontra bekötése (controllers/login.js)

router.get('/signup', function (req, res) {
    res.render('login/signup');
});

5. lépés

Munkamenetbe sorosítás

passport.serializeUser(function(user, done) {
    done(null, user);
});

passport.deserializeUser(function(obj, done) {
    done(null, obj);
});

6. lépés

Passport lokális stratégiájának beállítása (regisztrálás)

npm install passport-local --save
var LocalStrategy = require('passport-local').Strategy;

// Local Strategy for sign-up
passport.use('local-signup', new LocalStrategy({
        usernameField: 'neptun',
        passwordField: 'password',
        passReqToCallback: true,
    },   
    function(req, neptun, password, done) {
        // Hiba ág
        return done(null, false, { message: 'Létező neptun.' });
        // Siker ág
        return done(null, {username: 'anonymous'});    
    }
));

7. lépés

Regisztrációs adatok fogadása és regisztráció

router.post('/signup', passport.authenticate('local-signup', {
    successRedirect:    '/login',
    failureRedirect:    '/login/signup',
    failureFlash:       true,
    badRequestMessage:  'Hiányzó adatok'
}));

8. lépés

Hibaüzenetek megjelenítése

Sablon

{{#each errorMessages}}
<div class="alert alert-dismissible alert-danger">
    <button type="button" class="close" data-dismiss="alert">×</button>
    {{this}}
</div>
{{/each}}

Vezérlő

router.get('/signup', function (req, res) {
    res.render('login/signup', {
        errorMessages: req.flash('error')
    });
});

9. lépés

User modell létrehozása és regisztrálása az ORM-ben

module.exports = {
    identity: 'user',
    connection: 'default',
    attributes: {
        neptun: {
            type: 'string',
            required: true,
            unique: true,
        },
        password: {
            type: 'string',
            required: true,
        },
        surname: {
            type: 'string',
            required: true,
        },
        forename: {
            type: 'string',
            required: true,
        },
        avatar: {
            type: 'string',
            url: true,
        },
        role: {
            type: 'string',
            enum: ['riporter', 'operator'],
            required: true,
            defaultsTo: 'riporter'
        },
        
        errors: {
            collection: 'error',
            via: 'user'
        },
    },
};

10. lépés

Az error modell módosítása a user modellel való összekapcsolás miatt

module.exports = {
    identity: 'error',
        /* ... */         
        user: {
            model: 'user',
        },
    }
};

11. lépés

Regisztrálás a user modell segítségével

function(req, neptun, password, done) {
    req.app.models.user.findOne({ neptun: neptun }, function(err, user) {
        if (err) { return done(err); }
        if (user) {
            return done(null, false, { message: 'Létező neptun.' });
        }
        req.app.models.user.create(req.body)
        .then(function (user) {
            return done(null, user);
        })
        .catch(function (err) {
            return done(null, false, { message: err.details });
        })
    });
}

12. lépés

Bejelentkezési stratégia

// Stratégia
passport.use('local', new LocalStrategy({
        usernameField: 'neptun',
        passwordField: 'password',
        passReqToCallback: true,
    },
    function(req, neptun, password, done) {
        req.app.models.user.findOne({ neptun: neptun }, function(err, user) {
            if (err) { return done(err); }
            if (!user || !user.validPassword(password)) {
                return done(null, false, { message: 'Helytelen adatok.' });
            }
            return done(null, user);
        });
    }
));

// User modell
module.exports = {
    identity: 'user',
    connection: 'default',
    attributes: {
        validPassword: function (password) {
            return password === this.password;
        }
    },
};

13. lépés

Stratégia alkalmazása

router.post('/', passport.authenticate('local', {
    successRedirect: '/errors/list',
    failureRedirect: '/login',
    failureFlash: true,
    badRequestMessage: 'Hiányzó adatok'
}));

14. lépés

Layout: ki-/bejelentkezés váltogatása

<ul class="nav navbar-nav navbar-right">
    {{#if loggedIn}}
    <li><a href="/logout">Kilépés</a></li>
    {{else}}
    <li><a href="/login">Bejelentkezés</a></li>
    {{/if}}
    <li><a href="#">About</a></li>
</ul>
{{#if loggedIn}}
<p class="navbar-text navbar-right">Üdv, {{user.forename}}!</p>
{{/if}}
// Middleware segédfüggvény
function setLocalsForLayout() {
    return function (req, res, next) {
        res.locals.loggedIn = req.isAuthenticated();
        res.locals.user = req.user;
        next();
    }
}
/* ... */
app.use(setLocalsForLayout());

15. lépés

/errors végpontok védelme

Segédfüggvény

function ensureAuthenticated(req, res, next) {
    if (req.isAuthenticated()) { return next(); }
    res.redirect('/login');
}

Használata

app.use('/errors', ensureAuthenticated, errorRouter);

16. lépés

Szerepkör szintű védelem

Pl. /operator végpontot csak az operator szerepkörűek érhetik el.

Segédfüggvény

function andRestrictTo(role) {
    return function(req, res, next) {
        if (req.user.role == role) {
            next();
        } else {
            next(new Error('Unauthorized'));
        }
    }
}
app.get('/operator', ensureAuthenticated, andRestrictTo('operator'), function(req, res) {
    res.end('operator');
});

17. lépés

Jelszó titkosítása

npm install bcryptjs --save
// User modell (models/user.js)
var bcrypt = require('bcryptjs');

module.exports = {
    identity: 'user',
    connection: 'default',
    attributes: {
        /* ... */        
        validPassword: function (password) {
            return bcrypt.compareSync(password, this.password);
        }
    },
    
    beforeCreate: function(values, next) {
        bcrypt.hash(values.password, 10, function(err, hash) {
            if (err) {
                return next(err);
            }
            values.password = hash;
            next();
        });
    }
};

18. lépés

Kijelentkezés

app.get('/logout', function(req, res){
    req.logout();
    res.redirect('/');
});

Feladat

Tedd lehetővé, hogy a Google/Facebook/Github account-oddal lépjél be!

1. lépés

Telepítés

npm install passport-google-oauth --save

2. lépés

Google beállítások

  • új projekt
  • Enable APIs
    • Google Plus API
  • Credentials
    • Add Credential
      • OAuth 2.0 Client id
  • Authorized redirect URIs beállítása

3. lépés

Stratégia

var GoogleStrategy = require('passport-google-oauth').OAuth2Strategy;

var GOOGLE_CLIENT_ID = "...";
var GOOGLE_CLIENT_SECRET = "...";

passport.use('google', new GoogleStrategy({
        clientID: GOOGLE_CLIENT_ID,
        clientSecret: GOOGLE_CLIENT_SECRET,
        callbackURL: "https://alkfejl-01-horvathgyozo.c9.io/login/return"
    },
    function(accessToken, refreshToken, profile, done) {
        console.log(profile)
        return done(null, profile);
    }
));

4. lépés

Végpontok beállítása

router.get('/google', passport.authenticate('google', {
    scope: 'https://www.googleapis.com/auth/plus.login'
}));

router.get('/return', passport.authenticate('google', { 
    successRedirect: '/errors/list',
    failureRedirect: '/' 
}));