Aplicaciones de una sola página con Vue.js y Flask: autenticación JWT

Bienvenido a la sexta entrega de esta serie de tutoriales de varias partes sobre el desarrollo web completo con Vue.js y Flask. En este post voy a demostrar...

Autenticación JWT

Bienvenido a la sexta entrega de esta serie de tutoriales de varias partes sobre el desarrollo web completo con Vue.js y Flask. En esta publicación, demostraré una forma de usar la autenticación JSON Web Token (JWT).

El código de esta publicación se puede encontrar en mi cuenta de GitHub en la rama sextopost.

Contenido de la serie

  1. Configuración y familiarización con VueJS
  2. Navegación por el enrutador Vue
  3. Gestión de estados con Vuex
  4. API RESTful con Flask
  5. Integración AJAX con API REST
  6. Autenticación JWT (usted está aquí)
  7. Implementación en un servidor privado virtual

Introducción básica a la autenticación JWT

Al igual que algunas de las otras publicaciones de esta serie, no entraré en detalles significativos sobre la teoría de cómo funciona [JWT] (https://en.wikipedia.org/wiki/JSON_Web_Token). En cambio, adoptaré un enfoque pragmático y demostraré sus detalles de implementación utilizando las tecnologías de interés dentro de Flask y Vue.js. Si está interesado en obtener una comprensión más profunda de los JWT, lo remito a la excelente publicación de Scott Robinson aquí en wikihtp, donde explica el bajo nivel detalles de la técnica.

En el sentido básico, un JWT es un objeto JSON codificado que se utiliza para transmitir información entre dos sistemas que se compone de un encabezado, una carga útil y una firma en forma de [HEADER].[PAYLOAD].[SIGNATURE], todo contenido en el encabezado HTTP como "Autorización: Portador [HEADER].[PAYLOAD].[SIGNATURE]". El proceso comienza cuando el cliente (sistema solicitante) se autentica con el servidor (un servicio con un recurso deseado) que genera un JWT que solo es válido por un período de tiempo específico. Luego, el servidor devuelve esto como un token firmado y codificado para que el cliente lo almacene y lo use para verificación en comunicaciones posteriores.

La autenticación JWT funciona bastante bien para aplicaciones SPA como la que se está creando en esta serie y ha ganado una popularidad significativa entre los desarrolladores que las implementan.

Implementación de la autenticación JWT en Flask RESTful API

En el lado de Flask, usaré el paquete Python [PyJWT] (https://github.com/jpadilla/pyjwt) para manejar algunos de los detalles relacionados con la creación, el análisis y la validación de JWT.

1
(venv) $ pip install PyJWT

Con el paquete PyJWT instalado, puedo pasar a implementar las piezas necesarias para la autenticación y verificación en la aplicación Flask. Para empezar, le daré a la aplicación la capacidad de crear nuevos usuarios registrados que estarán representados por una clase Usuario. Al igual que con todas las demás clases de esta aplicación, la clase Usuario residirá en el módulo models.py.

Lo primero que debe hacer es importar un par de funciones, generate_password_hash y check_password_hash del módulo security del paquete werkzeug que usaré para generar y verificar contraseñas hash. No es necesario instalar este paquete, ya que viene con Flask automáticamente.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
"""
models.py
- Data classes for the surveyapi application
"""

from datetime import datetime
from flask_sqlalchemy import SQLAlchemy

from werkzeug.security import generate_password_hash, check_password_hash

db = SQLAlchemy()

Directamente debajo del código anterior, defino la clase ‘Usuario’, que hereda de la clase ‘Modelo’ de SQLAlchemy, similar a las otras definidas en publicaciones anteriores. Esta clase de ‘Usuario’ debe contener un campo de clase de clave principal de entero generado automáticamente llamado ‘id’ y luego dos campos de cadena llamados ‘correo electrónico’ y ‘contraseña’ con el correo electrónico configurado para ser único. También doy a esta clase un campo de “relación” para asociar cualquier encuesta que el usuario pueda crear. En el otro lado de esta ecuación, agregué una clave externa creator_id a la clase Survey para vincular a los usuarios con las encuestas que crean.

Anulo el método __init__(...) para poder codificar la contraseña al instanciar un nuevo objeto Usuario. Después de eso, le doy el método de clase, authenticate, para consultar a un usuario por correo electrónico y verificar que el hash de la contraseña proporcionada coincida con el almacenado en la base de datos. Si coinciden, devuelvo el usuario autenticado. Por último, pero no menos importante, agregué un método to_dict() para ayudar a serializar los objetos del usuario.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
"""
models.py
- Data classes for the surveyapi application
"""

#
# omitting imports and what not
#

class User(db.Model):
    __tablename__ = 'users'

    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(120), unique=True, nullable=False)
    password = db.Column(db.String(255), nullable=False)
    surveys = db.relationship('Survey', backref="creator", lazy=False)

    def __init__(self, email, password):
        self.email = email
        self.password = generate_password_hash(password, method='sha256')

    @classmethod
    def authenticate(cls, **kwargs):
        email = kwargs.get('email')
        password = kwargs.get('password')
        
        if not email or not password:
            return None

        user = cls.query.filter_by(email=email).first()
        if not user or not check_password_hash(user.password, password):
            return None

        return user

    def to_dict(self):
        return dict(id=self.id, email=self.email)

class Survey(db.Model):
    __tablename__ = 'surveys'

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.Text)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    questions = db.relationship('Question', backref="survey", lazy=False)
    creator_id = db.Column(db.Integer, db.ForeignKey('users.id'))

    def to_dict(self):
      return dict(id=self.id,
                  name=self.name,
                  created_at=self.created_at.strftime('%Y-%m-%d %H:%M:%S'),
                  questions=[question.to_dict() for question in self.questions])

El siguiente paso es generar una nueva migración y actualizar la base de datos con ella para emparejar la clase Usuario de Python con una tabla de base de datos de usuarios sqlite. Para hacer esto, ejecuto los siguientes comandos en el mismo directorio que mi módulo manage.py.

1
2
(venv) $ python manage.py db migrate
(venv) $ python manage.py db upgrade

Bien, es hora de saltar al módulo api.py e implementar la funcionalidad para registrar y autenticar a los usuarios junto con la funcionalidad de verificación para proteger la creación de nuevas encuestas. Después de todo, no quiero que ningún robot web nefasto u otros malos actores contaminen mi increíble aplicación de encuestas.

Para comenzar, agrego la clase Usuario a la lista de importaciones desde el módulo models.py hacia la parte superior del módulo api.py. Mientras estoy allí, seguiré adelante y agregaré un par de otras importaciones que usaré más adelante.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
"""
api.py
- provides the API endpoints for consuming and producing 
  REST requests and responses
"""

from functools import wraps
from datetime import datetime, timedelta

from flask import Blueprint, jsonify, request, current_app

import jwt

from .models import db, Survey, Question, Choice, User

Ahora que tengo todas las herramientas que necesito importadas, puedo implementar un conjunto de funciones de vista de registro e inicio de sesión en el módulo api.py.

Comenzaré con la función de vista register() que espera que se envíe un correo electrónico y una contraseña en JSON en el cuerpo de la solicitud POST. El usuario simplemente se crea con lo que se proporciona para el correo electrónico y la contraseña y felizmente devuelvo una respuesta JSON (que no es necesariamente el mejor enfoque, pero funcionará por el momento).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
"""
api.py
- provides the API endpoints for consuming and producing 
  REST requests and responses
"""

#
# omitting inputs and other view functions
#

@api.route('/register/', methods=('POST',))
def register():
    data = request.get_json()
    user = User(**data)
    db.session.add(user)
    db.session.commit()
    return jsonify(user.to_dict()), 201

Enfriar. El backend es capaz de crear nuevos usuarios deseosos de crear montones de encuestas, así que mejor agrego alguna funcionalidad para autenticarlos y dejar que sigan creando sus encuestas.

La función de inicio de sesión utiliza el método de clase User.authenticate(...) para intentar encontrar y autenticar a un usuario. Si se encuentra el usuario que coincide con el correo electrónico y la contraseña proporcionados, la función de inicio de sesión avanza para crear un token JWT; de lo contrario, se devuelve Ninguno, lo que hace que la función de inicio de sesión devuelva un mensaje de "fallo de autenticación" con el estado HTTP apropiado código de 401.

Creo el token JWT usando PyJWT (como jwt) codificando un diccionario que contiene lo siguiente:

  • sub - el asunto del jwt, que en este caso es el correo electrónico del usuario
  • iat - la hora en que se emitió el jwt
  • exp - es el momento en que debe expirar el jwt, que es 30 minutos después de su emisión en este caso
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
"""
api.py
- provides the API endpoints for consuming and producing 
  REST requests and responses
"""

#
# omitting inputs and other view functions
#

@api.route('/login/', methods=('POST',))
def login():
    data = request.get_json()
    user = User.authenticate(**data)

    if not user:
        return jsonify({ 'message': 'Invalid credentials', 'authenticated': False }), 401

    token = jwt.encode({
        'sub': user.email,
        'iat':datetime.utcnow(),
        'exp': datetime.utcnow() + timedelta(minutes=30)},
        current_app.config['SECRET_KEY'])
    return jsonify({ 'token': token.decode('UTF-8') })

El proceso de codificación utiliza el valor de la propiedad SECRET_KEY de la clase BaseConfig definida en config.py y se mantiene en la propiedad de configuración current_app una vez que se crea la aplicación Flask.

A continuación, me gustaría dividir la funcionalidad GET y POST que actualmente reside en una función de vista mal nombrada llamada fetch_survey(...) que se muestra a continuación en su estado original. En su lugar, dejaré que fetch_surveys(...) se encargue únicamente de obtener todas las encuestas cuando solicite "/api/surveys/" con una solicitud GET. La creación de encuestas, por otro lado, que ocurre cuando la misma URL recibe una solicitud POST, ahora residirá en una nueva función llamada create_survey(...).

Así que esto...

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
"""
api.py
- provides the API endpoints for consuming and producing 
  REST requests and responses
"""

#
# omitting inputs and other view functions
#

@api.route('/surveys/', methods=('GET', 'POST'))
def fetch_surveys():
    if request.method == 'GET':
        surveys = Survey.query.all()
        return jsonify([s.to_dict() for s in surveys])
    elif request.method == 'POST':
        data = request.get_json()
        survey = Survey(name=data['name'])
        questions = []
        for q in data['questions']:
            question = Question(text=q['question'])
            question.choices = [Choice(text=c) for c in q['choices']]
            questions.append(question)
        survey.questions = questions
        db.session.add(survey)
        db.session.commit()
        return jsonify(survey.to_dict()), 201

se convierte en esto...

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
"""
api.py
- provides the API endpoints for consuming and producing 
  REST requests and responses
"""

#
# omitting inputs and other view functions
#

@api.route('/surveys/', methods=('POST',))
def create_survey(current_user):
    data = request.get_json()
    survey = Survey(name=data['name'])
    questions = []
    for q in data['questions']:
        question = Question(text=q['question'])
        question.choices = [Choice(text=c) for c in q['choices']]
        questions.append(question)
    survey.questions = questions
    survey.creator = current_user
    db.session.add(survey)
    db.session.commit()
    return jsonify(survey.to_dict()), 201


@api.route('/surveys/', methods=('GET',))
def fetch_surveys():
    surveys = Survey.query.all()
    return jsonify([s.to_dict() for s in surveys])

La verdadera clave ahora es proteger la función de vista create_survey(...) para que solo los usuarios autenticados puedan crear nuevas encuestas. Dicho de otra manera, si se realiza una solicitud POST contra "/api/surveys", la aplicación debe verificar que la esté realizando un usuario válido y autenticado.

¡Llega el útil decorador Python! Usaré un decorador para envolver la función de vista create_survey(...) que comprobará que el solicitante contiene un token JWT válido en su encabezado y rechazará cualquier solicitud que no lo tenga. Llamaré a este decorador token_required y lo implementaré sobre todas las demás funciones de vista en api.py así:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
"""
api.py
- provides the API endpoints for consuming and producing 
  REST requests and responses
"""

#
# omitting inputs and other view functions
#

def token_required(f):
    @wraps(f)
    def _verify(*args, **kwargs):
        auth_headers = request.headers.get('Authorization', '').split()

        invalid_msg = {
            'message': 'Invalid token. Registeration and / or authentication required',
            'authenticated': False
        }
        expired_msg = {
            'message': 'Expired token. Reauthentication required.',
            'authenticated': False
        }

        if len(auth_headers) != 2:
            return jsonify(invalid_msg), 401

        try:
            token = auth_headers[1]
            data = jwt.decode(token, current_app.config['SECRET_KEY'])
            user = User.query.filter_by(email=data['sub']).first()
            if not user:
                raise RuntimeError('User not found')
            return f(user, *args, **kwargs)
        except jwt.ExpiredSignatureError:
            return jsonify(expired_msg), 401 # 401 is Unauthorized HTTP status code
        except (jwt.InvalidTokenError, Exception) as e:
            print(e)
            return jsonify(invalid_msg), 401

    return _verify

La lógica principal de este decorador es:

  1. Asegúrese de que contiene el encabezado "Autorización" con una cadena que parece un token JWT
  2. Valide que el JWT no esté vencido, de lo cual PyJWT se encargará lanzando un ExpiredSignatureError si ya no es válido
  3. Valide que el JWT sea un token válido, del cual PyJWT también se encarga lanzando un InvalidTokenError si no es válido
  4. Si todo es válido, se consulta al usuario asociado desde la base de datos y se devuelve a la función que el decorador está ajustando.

Ahora todo lo que queda es agregar el decorador al método create_survey(...) así:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
"""
api.py
- provides the API endpoints for consuming and producing 
  REST requests and responses
"""

#
# omitting inputs and other functions
#

@api.route('/surveys/', methods=('POST',))
@token_required
def create_survey(current_user):
    data = request.get_json()
    survey = Survey(name=data['name'])
    questions = []
    for q in data['questions']:
        question = Question(text=q['question'])
        question.choices = [Choice(text=c) for c in q['choices']]
        questions.append(question)
    survey.questions = questions
    survey.creator = current_user
    db.session.add(survey)
    db.session.commit()
    return jsonify(survey.to_dict()), 201

Implementación de la autenticación JWT en Vue.js SPA

Con el lado de back-end de la ecuación de autenticación completa, ahora necesito abotonar el lado del cliente implementando la autenticación JWT en Vue.js. Comienzo creando un nuevo módulo dentro de la aplicación llamado "utils" dentro del directorio src y colocando un archivo index.js dentro de la carpeta utils. Este módulo contendrá dos cosas:

  1. Un bus de eventos que puedo usar para enviar mensajes a través de la aplicación cuando suceden ciertas cosas, como una autenticación fallida en el caso de un JWT caducado.
  2. Una función para verificar un JWT para ver si todavía es válido o no

Estas dos cosas se implementan así:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// utils/index.js

import Vue from 'vue'

export const EventBus = new Vue()

export function isValidJwt (jwt) {
  if (!jwt || jwt.split('.').length < 3) {
    return false
  }
  const data = JSON.parse(atob(jwt.split('.')[1]))
  const exp = new Date(data.exp * 1000) // JS deals with dates in milliseconds since epoch
  const now = new Date()
  return now < exp
}

La variable EventBus es solo una instancia del objeto Vue. Puedo utilizar el hecho de que el objeto Vue tiene tanto un $emit como un par de métodos $on / $off, que se utilizan para emitir eventos, así como para registrar y cancelar el registro de eventos.

La función isValid(jwt) es lo que usaré para determinar si un usuario está autenticado en base a la información en el JWT. Recuerde de la explicación básica anterior de los JWT que un conjunto estándar de propiedades reside en un objeto JSON codificado de la forma "[HEADER].[PAYLOAD].[SIGNATURE]". Por ejemplo, digamos que tengo el siguiente JWT:

1
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJleGFtcGxlQG1haWwuY29tIiwiaWF0IjoxNTIyMzI2NzMyLCJleHAiOjE1MjIzMjg1MzJ9.1n9fx0vL9GumDGatwm2vfUqQl3yZ7Kl4t5NWMvW-pgw

Puedo decodificar la sección del medio del cuerpo para inspeccionar su contenido usando el siguiente JavaScript:

1
2
3
4
const token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJleGFtcGxlQG1haWwuY29tIiwiaWF0IjoxNTIyMzI2NzMyLCJleHAiOjE1MjIzMjg1MzJ9.1n9fx0vL9GumDGatwm2vfUqQl3yZ7Kl4t5NWMvW-pgw'
const tokenParts = token.split('.')
const body = JSON.parse(atob(tokenParts[1]))
console.log(body)   // {sub: "[correo electrónico protegido]", iat: 1522326732, exp: 1522328532}

Aquí, el contenido del cuerpo del token es sub, que representa el correo electrónico del suscriptor, iat, que se emite en la marca de tiempo en segundos, y exp, que es el tiempo en el que expirará el token en segundos desde la época. (el número de segundos transcurridos desde el 1 de enero de 1970 (medianoche UTC/GMT), sin contar los segundos bisiestos (en ISO 8601: 1970-01-01T00:00:00Z)). Como puede ver, estoy usando el valor exp en la función isValidJwt (jwt) para determinar si el JWT está vencido o no.

El siguiente paso es agregar un par de nuevas funciones AJAX para realizar llamadas a la API REST de Flask para registrar nuevos usuarios e iniciar sesión en los existentes, además tendré que modificar la función postNewSurvey (...) para incluir un encabezado que contenga un JWT.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// api/index.js

//
// omitting stuff ... skipping to the bottom of the file
//

export function postNewSurvey (survey, jwt) {
  return axios.post(`${API_URL}/surveys/`, survey, { headers: { Authorization: `Bearer: ${jwt}` } })
}

export function authenticate (userData) {
  return axios.post(`${API_URL}/login/`, userData)
}

export function register (userData) {
  return axios.post(`${API_URL}/register/`, userData)
}

Ok, ahora puedo usar estas cosas en la tienda para administrar el estado requerido para proporcionar la funcionalidad de autenticación adecuada. Para comenzar, importo las funciones EventBus y isValidJwt(...) del módulo utils, así como las dos nuevas funciones AJAX del módulo api. A continuación, agregue una definición de un objeto ‘usuario’ y una cadena de token ‘jwt’ en el objeto de estado de la tienda de la siguiente manera:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// store/index.js

import Vue from 'vue'
import Vuex from 'vuex'

// imports of AJAX functions will go here
import { fetchSurveys, fetchSurvey, saveSurveyResponse, postNewSurvey, authenticate, register } from '@/api'
import { isValidJwt, EventBus } from '@/utils'

Vue.use(Vuex)

const state = {
  // single source of data
  surveys: [],
  currentSurvey: {},
  user: {},
  jwt: ''
}

//
// omitting all the other stuff below
//

A continuación, necesito agregar un par de métodos de acción que llamarán a las funciones AJAX register(...) o authenticate(...) que acabamos de definir. Nombro al responsable de autenticar a un usuario login(...), que llama a la función AJAX authenticate(...) y cuando devuelve una respuesta exitosa que contiene un nuevo JWT comete una mutación que nombraré setJwtToken, que debe agregarse al objeto de mutaciones. En el caso de una solicitud de autenticación fallida, encadeno un método catch a la cadena de promesa para detectar el error y uso el EventBus para emitir un evento que notifique a los suscriptores que la autenticación falló.

El método de acción register(...) es bastante similar a login(...), de hecho, en realidad utiliza login(...). También estoy mostrando una pequeña modificación al método de acción submitNewSurvey(...) que pasa el token JWT como un parámetro adicional a la llamada AJAX postNewSurvey(...).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const actions = {
  // asynchronous operations

  //
  // omitting the other action methods...
  //

  login (context, userData) {
    context.commit('setUserData', { userData })
    return authenticate(userData)
      .then(response => context.commit('setJwtToken', { jwt: response.data }))
      .catch(error => {
        console.log('Error Authenticating: ', error)
        EventBus.$emit('failedAuthentication', error)
      })
  },
  register (context, userData) {
    context.commit('setUserData', { userData })
    return register(userData)
      .then(context.dispatch('login', userData))
      .catch(error => {
        console.log('Error Registering: ', error)
        EventBus.$emit('failedRegistering: ', error)
      })
  },
  submitNewSurvey (context, survey) {
    return postNewSurvey(survey, context.state.jwt.token)
  }
}

Como se mencionó anteriormente, necesito agregar una nueva mutación que establezca explícitamente el JWT y los datos del usuario.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
const mutations = {
  // isolated data mutations

  //
  // omitting the other mutation methods...
  //

  setUserData (state, payload) {
    console.log('setUserData payload = ', payload)
    state.userData = payload.userData
  },
  setJwtToken (state, payload) {
    console.log('setJwtToken payload = ', payload)
    localStorage.token = payload.jwt.token
    state.jwt = payload.jwt
  }
}

Lo último que me gustaría hacer en la tienda es agregar un método getter que se llamará en un par de otros lugares en la aplicación que indicará si el usuario actual está autenticado o no. Logro esto llamando a la función isValidJwt(jwt) desde el módulo utils dentro del captador así:

1
2
3
4
5
6
const getters = {
  // reusable data accessors
  isAuthenticated (state) {
    return isValidJwt(state.jwt.token)
  }
}

Ok, me estoy acercando. Necesito agregar un nuevo componente Vue.js para una página de inicio de sesión/registro en la aplicación. Creo un archivo llamado Login.vue en el directorio de componentes. En la sección de plantilla le doy dos campos de entrada, uno para un correo electrónico, que servirá como nombre de usuario, y otro para la contraseña. Debajo de ellos hay dos botones, uno para iniciar sesión si ya es un usuario registrado y otro para registrarse.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<!-- components/Login.vue -->
<template>
  <div>
    <section class="hero is-primary">
      <div class="hero-body">
        <div class="container has-text-centered">
          <h2 class="title">Login or Register</h2>
          <p class="subtitle error-msg">{{ errorMsg }}</p>
        </div>
      </div>
    </section>
    <section class="section">
      <div class="container">
        <div class="field">
          <label class="label is-large" for="email">Email:</label>
          <div class="control">
            <input type="email" class="input is-large" id="email" v-model="email">
          </div>
        </div>
        <div class="field">
          <label class="label is-large" for="password">Password:</label>
          <div class="control">
            <input type="password" class="input is-large" id="password" v-model="password">
          </div>
        </div>

        <div class="control">
          <a class="button is-large is-primary" @click="authenticate">Login</a>
          <a class="button is-large is-success" @click="register">Register</a>
        </div>

      </div>
    </section>

  </div>
</template>

Obviamente, este componente necesitará algún estado local asociado con un usuario como lo indica mi uso de v-model en los campos de entrada, así que lo agrego en la propiedad de datos del componente a continuación. También agrego una propiedad de datos errorMsg que contendrá cualquier mensaje emitido por EventBus en caso de que falle el registro o la autenticación. Para utilizar EventBus, me suscribo a los eventos 'failedRegistering' y 'failedAuthentication' en la etapa del ciclo de vida del componente montado de Vue.js, y anulo el registro en la etapa beforeDestroy. Otra cosa a tener en cuenta es el uso de controladores de eventos @click que se invocan al hacer clic en los botones Iniciar sesión y Registrarse. Esos deben implementarse como métodos de componentes, authenticate() y register().

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<!-- components/Login.vue -->
<script>
export default {
  data () {
    return {
      email: '',
      password: '',
      errorMsg: ''
    }
  },
  methods: {
    authenticate () {
      this.$store.dispatch('login', { email: this.email, password: this.password })
        .then(() => this.$router.push('/'))
    },
    register () {
      this.$store.dispatch('register', { email: this.email, password: this.password })
        .then(() => this.$router.push('/'))
    }
  },
  mounted () {
    EventBus.$on('failedRegistering', (msg) => {
      this.errorMsg = msg
    })
    EventBus.$on('failedAuthentication', (msg) => {
      this.errorMsg = msg
    })
  },
  beforeDestroy () {
    EventBus.$off('failedRegistering')
    EventBus.$off('failedAuthentication')
  }
}
</script>

Ok, ahora solo necesito que el resto de la aplicación sepa que el componente de inicio de sesión existe. Hago esto importándolo en el módulo del enrutador y definiendo su ruta. Mientras estoy en el módulo del enrutador, necesito hacer un cambio adicional en la ruta del componente NewSurvey para proteger su acceso solo a usuarios autenticados, como se muestra a continuación:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import Home from '@/components/Home'
import Survey from '@/components/Survey'
import NewSurvey from '@/components/NewSurvey'
import Login from '@/components/Login'
import store from '@/store'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'Home',
      component: Home
    }, {
      path: '/surveys/:id',
      name: 'Survey',
      component: Survey
    }, {
      path: '/surveys',
      name: 'NewSurvey',
      component: NewSurvey,
      beforeEnter (to, from, next) {
        if (!store.getters.isAuthenticated) {
          next('/login')
        } else {
          next()
        }
      }
    }, {
      path: '/login',
      name: 'Login',
      component: Login
    }
  ]
})

Vale la pena mencionar aquí que estoy utilizando la protección de ruta de vue-router beforeEnter para verificar si el usuario actual está autenticado a través del captador isAuthenticated de la tienda. Si isAuthenticated devuelve falso, redirijo la aplicación a la página de inicio de sesión.

Con el componente de inicio de sesión codificado y su ruta definida, puedo proporcionar acceso a él a través de un componente de enlace de enrutador en el componente de encabezado dentro de components/Header.vue. Condicionalmente, muestro el enlace al componente NewSurvey o al componente Login utilizando el captador de almacenamiento isAuthenticated una vez más dentro de una propiedad calculada en el componente Header al que hacen referencia las directivas v-if así:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<!-- components/Header.vue -->
<template>
<nav class="navbar is-light" role="navigation" aria-label="main navigation">
  <div class="navbar-menu">
    <div class="navbar-start">
      <router-link to="/" class="navbar-item">
        Home
      </router-link>
      <router-link v-if="isAuthenticated" to="/surveys" class="navbar-item">
        Create Survey
      </router-link>
      <router-link v-if="!isAuthenticated" to="/login" class="navbar-item">
        Login / Register
      </router-link>
    </div>
  </div>
</nav>
</template>

<script>
export default {
  computed: {
    isAuthenticated () {
      return this.$store.getters.isAuthenticated
    }
  }
}
</script>

<style>

</style>

¡Excelente! Ahora finalmente puedo encender los servidores de desarrollo para la aplicación Flask y la aplicación Vue.js y probar para ver si puedo registrarme e iniciar sesión como usuario.

Primero inicio el servidor de desarrollo de Flask.

1
(venv) $ python appserver.py

Luego, el servidor de desarrollo webpack para compilar y servir la aplicación Vue.js.

1
$ npm run dev

En mi navegador, visito http://localhost:8080 (o cualquier puerto que indique el servidor de desarrollo webpack) y me aseguro de que la barra de navegación ahora muestre "Iniciar sesión/Registrarse" en lugar de "Crear encuesta" como se muestra abajo:

Login navbar{.img-responsive}

A continuación, hago clic en el enlace "Iniciar sesión/Registrarse" y completé las entradas para un correo electrónico y una contraseña, luego hago clic en registrarme para asegurarme de que funciona como se espera y me redirigen de nuevo a la página de inicio y veo "Crear encuesta". " enlace que se muestra en lugar del "Iniciar sesión/Registrarse" que estaba allí antes de registrarse.

Login page{.img-responsive}

Muy bien, mi trabajo está hecho en gran parte. Lo único que queda por hacer es agregar un pequeño manejo de errores al método submitSurvey(...) Vue.js del componente NewSurvey para manejar el evento en el que un token caduca mientras el usuario está creando un nuevo encuesta así:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<script>
import NewQuestion from '@/components/NewQuestion'

export default {
  components: { NewQuestion },
  data () {
    return {
      step: 'name',
      name: '',
      questions: []
    }
  },
  methods: {

    //
    // omitting other methods
    //

    submitSurvey () {
      this.$store.dispatch('submitNewSurvey', {
        name: this.name,
        questions: this.questions
      })
        .then(() => this.$router.push('/'))
        .catch((error) => {
          console.log('Error creating survey', error)
          this.$router.push('/')
        })
    }
  }
}
</script>

Recursos

¿Quiere obtener más información sobre los diversos marcos utilizados en este artículo? Intente consultar algunos de los siguientes recursos para profundizar en el uso de Vue.js o la creación de API de back-end en Python:

Conclusión

En esta publicación, demostré cómo implementar la autenticación JWT en la aplicación de encuestas usando Vue.js y Flask. JWT es un método popular y robusto para proporcionar autenticación dentro de las aplicaciones SPA, y espero que después de leer esta publicación se sienta cómodo usando estas tecnologías para proteger sus aplicaciones. Sin embargo, recomiendo visitar el artículo Abuso de pila de Scott para una comprensión más profunda de cómo y por qué funciona JWT.

Como siempre, gracias por leer y no se avergüence de comentar o criticar a continuación.