Single Page Apps con Vue.js y Flask: Autenticaci贸n JWT

    Autenticaci贸n JWT

    Bienvenido a la sexta entrega de esta serie de tutoriales de varias partes sobre desarrollo web full-stack usando Vue.js y Flask. En esta publicaci贸n, mostrar茅 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 SixthPost .

    Contenido de la serie

    • Seup y familiarizaci贸n con VueJS
    • Navegando por el enrutador Vue
    • Gesti贸n de estado con Vuex
    • API RESTful con Flask
    • Integraci贸n AJAX con API REST
    • Autenticaci贸n JWT (est谩s aqu铆)
    • Implementaci贸n en un servidor privado virtual

    Introducci贸n b谩sica a la autenticaci贸n JWT

    Al igual que en algunas de las otras publicaciones de esta serie, no entrar茅 en detalles significativos sobre la teor铆a de c贸mo funciona JWT . En cambio, adoptar茅 un enfoque pragm谩tico y demostrar茅 los detalles de su 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 Pharos.sh, donde explica los detalles de bajo nivel 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 lo contenido en el encabezado HTTP como “Autorizaci贸n: Portador [HEADER]. [PAYLOAD]. [FIRMA] “. 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 lo devuelve 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谩 construyendo en esta serie y ha ganado una popularidad significativa entre los desarrolladores que las implementan.

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

    En el lado de Flask, usar茅 el paquete Python PyJWT para manejar algunos de los detalles relacionados con la creaci贸n, an谩lisis y validaci贸n de JWT.

    (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 Userclase. Al igual que con todas las dem谩s clases de esta aplicaci贸n, la Userclase residir谩 en el m贸dulo models.py.

    El primer elemento que debe hacer es importar un par de funciones, generate_password_hashy check_password_hashdesde el m贸dulo del paquete werkzeug que usar茅security para generar y verificar contrase帽as hash. No es necesario instalar este paquete, ya que viene con Flask autom谩ticamente.

    """
    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 Userclase, que hereda de la Modelclase SQLAlchemy similar a las otras definidas en publicaciones anteriores. Esta Userclase debe contener un campo de clase de clave primaria de entero generado autom谩ticamente llamado idluego dos campos de cadena llamados emaily passwordcon el correo electr贸nico configurado para ser 煤nico. Tambi茅n le doy a esta clase un relationshipcampo para asociar cualquier encuesta que el usuario pueda crear. En el otro lado de esta ecuaci贸n, agregu茅 una creator_idclave externa a la Surveyclase para vincular a los usuarios con las encuestas que crean.

    Anulo el __init__(...)m茅todo para poder aplicar hash a la contrase帽a al crear una instancia de un nuevo Userobjeto. 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 contrase帽a proporcionado coincide con el almacenado en la base de datos. Si coinciden, devuelvo el usuario autenticado. Por 煤ltimo, pero no menos importante, agregu茅 un to_dict()m茅todo para ayudar a serializar objetos de usuario.

    """
    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 para emparejar la Userclase Python con una tabla de base de datos sqlite de usuarios. Para hacer esto, ejecuto los siguientes comandos en el mismo directorio que mi m贸dulo manage.py.

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

    Ok, es hora de saltar al m贸dulo api.py e implementar la funcionalidad para registrar y autenticar 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 infame u otros malos actores contaminen mi incre铆ble aplicaci贸n de encuestas.

    Para comenzar, agrego la Userclase a la lista de importaciones desde el m贸dulo models.py hacia la parte superior del m贸dulo api.py. Mientras est茅 all铆, seguir茅 adelante y agregar茅 un par de otras importaciones que usar茅 m谩s adelante.

    """
    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 register()funci贸n de vista 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 proporcione 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).

    """
    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
    
    

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

    La funci贸n de inicio de sesi贸n utiliza el User.authenticate(...)m茅todo de clase para intentar encontrar y autenticar a un usuario. Si se encuentra el usuario que coincide con el correo electr贸nico y la contrase帽a dados, la funci贸n de inicio de sesi贸n progresa para crear un token JWT; de lo contrario, Nonese devuelve, lo que da como resultado que la funci贸n de inicio de sesi贸n devuelva un mensaje de “falla en la autenticaci贸n” con el c贸digo de estado HTTP apropiado 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 – el momento en que se emiti贸 el jwt en
    • exp – es el momento en que el jwt debe expirar, que es 30 minutos despu茅s de emitido en este caso
    """
    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 de la BaseConfigclase SECRET_KEYdefinida en config.py y se mantiene en la current_apppropiedad de configuraci贸n de la ‘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 que se fetch_survey(...)muestra a continuaci贸n en su estado original. En su lugar, dejar茅 que fetch_surveys(...)sea 鈥嬧媏l 煤nico responsable de obtener todas las encuestas cuando solicite “/ api / survey /” con una solicitud GET. La creaci贸n de encuestas, por otro lado, que ocurre cuando se accede a la misma URL con una solicitud POST, ahora residir谩 en una nueva funci贸n llamada create_survey(...).

    As铆 que esto…

    """
    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 …

    """
    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 clave real ahora es proteger la create_survey(...)funci贸n de visualizaci贸n para que solo los usuarios autenticados puedan crear nuevas encuestas. Dicho de otra manera, si se realiza una solicitud POST contra “/ api / survey”, la aplicaci贸n debe verificar para asegurarse de que la realiza un usuario v谩lido y autenticado.

    隆Llega el pr谩ctico decorador de Python! Usar茅 un decorador para ajustar la create_survey(...)funci贸n de vista que verificar谩 que el solicitante contenga un token JWT v谩lido en su encabezado y rechazar谩 cualquier solicitud que no lo tenga. Llamar茅 a este decorador token_requiredy lo implementar茅 sobre todas las otras funciones de vista en api.py as铆:

    """
    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:

    • Aseg煤rese de que contenga el encabezado “Autorizaci贸n” con una cadena que parezca un token JWT
    • Validar que el JWT no est茅 vencido, lo que PyJWT se encarga por m铆 lanzando un ExpiredSignatureErrorsi ya no es v谩lido
    • Valide que el JWT es un token v谩lido, del cual PyJWT tambi茅n se encarga lanzando un InvalidTokenErrorsi no es v谩lido
    • Si todo es v谩lido, el usuario asociado es consultado desde la base de datos y regresado a la funci贸n que el decorador est谩 envolviendo.

    Ahora todo lo que queda es agregar el decorador al create_survey(...)m茅todo de esta manera:

    """
    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 del back-end, la ecuaci贸n de autenticaci贸n completa, ahora necesito abrochar el lado del cliente implementando la autenticaci贸n JWT en Vue.js. Empiezo 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:

    • Un bus de eventos que puedo usar para enviar mensajes alrededor de la aplicaci贸n cuando suceden ciertas cosas, como autenticaci贸n fallida en el caso de un JWT caducado
    • Una funci贸n para verificar un JWT para ver si todav铆a es v谩lido o no

    Estas dos cosas se implementan as铆:

    // 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 EventBusvariable es solo una instancia del objeto Vue. Puedo utilizar el hecho de que el objeto Vue tiene un $emity un par de m茅todos $on/ $off, que se utilizan para emitir eventos, as铆 como para registrar y anular el registro de eventos.

    El isValid(jwt)functino 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, decir que tengo la siguiente JWT ‘eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJleGFtcGxlQG1haWwuY29tIiwiaWF0IjoxNTIyMzI2NzMyLCJleHAiOjE1MjIzMjg1MzJ9.1n9fx0vL9GumDGatwm2vfUqQl3yZ7Kl4t5NWMvW-pgw’. Puedo decodificar la secci贸n del cuerpo medio para inspeccionar su contenido usando el siguiente JavaScript:

    const token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJleGFtcGxlQG1haWwuY29tIiwiaWF0IjoxNTIyMzI2NzMyLCJleHAiOjE1MjIzMjg1MzJ9.1n9fx0vL9GumDGatwm2vfUqQl3yZ7Kl4t5NWMvW-pgw'
    const tokenParts = token.split('.')
    const body = JSON.parse(atob(tokenParts[1]))
    console.log(body)   // {sub: "[email聽protected]", iat: 1522326732, exp: 1522328532}
    

    Aqu铆 el contenido del cuerpo del token es sub, que representa el correo electr贸nico del suscriptor, iatque se emite en la marca de tiempo en segundos, y expque es el tiempo en el que el token caducar谩 en segundos desde la 茅poca (la cantidad de segundos que han transcurrido desde 1 de enero de 1970 (medianoche UTC / GMT), sin contar los segundos intercalares (en ISO 8601: 1970-01-01T00: 00: 00Z)). Como puede ver, estoy usando el expvalor en la isValidJwt(jwt)funci贸n para determinar si el JWT est谩 vencido o no.

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

    
    // 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)
    }
    

    Bien, ahora puedo usar estas cosas en la tienda para administrar el estado requerido para proporcionar la funcionalidad de autenticaci贸n adecuada. Para comenzar, importo EventBusy isValidJwt(...)funciono desde el m贸dulo utils, as铆 como las dos nuevas funciones AJAX del m贸dulo api. Luego agregue una definici贸n de un userobjeto y una jwtcadena de token en el objeto de estado de la tienda as铆:

    // 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, tengo que a帽adir en un par de m茅todos de acci贸n que se llame, ya sea el register(...)o authenticate(...)funciones AJAX que acabamos de definir. Nombro al responsable de autenticar a un usuario login(...), que llama a la authenticate(...)funci贸n AJAX 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 catchm茅todo a la cadena de promesa para detectar el error y utilizar EventBuspara emitir un evento que notifique a los suscriptores que la autenticaci贸n fall贸.

    El register(...)m茅todo de acci贸n es bastante similar al que login(...), de hecho, utiliza login(...). Tambi茅n estoy mostrando una peque帽a modificaci贸n al submitNewSurvey(...)m茅todo de acci贸n que pasa el token JWT como par谩metro adicional a la postNewSurvey(...)llamada AJAX.

    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.

    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 de obtenci贸n que se llamar谩 en un par de otros lugares de la aplicaci贸n que indicar谩 si el usuario actual est谩 autenticado o no. Lo logro llamando a la isValidJwt(jwt)funci贸n desde el m贸dulo utils dentro del getter as铆:

    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 hay dos botones, uno para iniciar sesi贸n si ya es un usuario registrado y otro para registrarse.

    <!-- 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 seg煤n lo indicado por mi uso de v-modelen los campos de entrada, as铆 que lo agrego en la propiedad de datos del componente a continuaci贸n. Tambi茅n agrego una errorMsgpropiedad de datos que contendr谩 cualquier mensaje emitido por el EventBusen caso de un registro o autenticaci贸n fallidos. Para utilizar, EventBusme suscribo a los eventos ‘failureRegistering’ y ‘failedAuthentication’ en la mountedetapa del ciclo de vida del componente Vue.js y anule el registro en la beforeDestroyetapa. Otra cosa a tener en cuenta es el uso de @clickcontroladores de eventos que se llaman al hacer clic en los botones Iniciar sesi贸n y Registrarse. Estos se implementar谩n como m茅todos de componentes, authenticate()y register().

    <!-- 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("https://Pharos.sh.com/"))
        },
        register () {
          this.$store.dispatch('register', { email: this.email, password: this.password })
            .then(() => this.$router.push("https://Pharos.sh.com/"))
        }
      },
      mounted () {
        EventBus.$on('failedRegistering', (msg) => {
          this.errorMsg = msg
        })
        EventBus.$on('failedAuthentication', (msg) => {
          this.errorMsg = msg
        })
      },
      beforeDestroy () {
        EventBus.$off('failedRegistering')
        EventBus.$off('failedAuthentication')
      }
    }
    </script>
    

    Bien, ahora solo necesito que el resto de la aplicaci贸n sepa que existe el componente de inicio de sesi贸n. 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 NewSurveyruta del componente para proteger su acceso solo a usuarios autenticados, como se muestra a continuaci贸n:

    // 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: "https://Pharos.sh.com/",
          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 beforeEnterpara verificar si el usuario actual est谩 autenticado a trav茅s del isAuthenticatedgetter de la tienda. Si isAuthenticateddevuelve 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 NewSurveycomponente o al Logincomponente utilizando el isAuthenticatedcaptador de tienda una vez m谩s dentro de una propiedad calculada en el Headercomponente al que hacen referencia las v-ifdirectivas de esta manera:

    <!-- 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 iniciar los servidores de desarrollo para la aplicaci贸n Flask y la aplicaci贸n Vue.js y probar para ver si puedo registrar e iniciar sesi贸n como usuario.

    Primero inicio el servidor de desarrollo de Flask.

    (venv) $ python appserver.py
    

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

    $ 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 el lugar de “Crear encuesta” como se muestra a continuaci贸n:

    A continuaci贸n, hago clic en el enlace “Iniciar sesi贸n / Registrarse” y completo las entradas para un correo electr贸nico y una contrase帽a, luego hago clic en registrarse para asegurarme de que funcione como se esperaba y me redirigen de nuevo a la p谩gina de inicio y veo el enlace “Crear encuesta” que se muestra en su lugar. del “Iniciar sesi贸n / Registrarse” que estaba all铆 antes de registrarse.

    Muy bien, mi trabajo est谩 terminado. Lo 煤nico que queda por hacer es agregar un peque帽o manejo de errores al submitSurvey(...)m茅todo Vue.js del NewSurveycomponente para manejar el evento donde un token expira mientras el usuario est谩 creando una nueva encuesta como esta:

    <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("https://Pharos.sh.com/"))
            .catch((error) => {
              console.log('Error creating survey', error)
              this.$router.push("https://Pharos.sh.com/")
            })
        }
      }
    }
    </script>
    

     

    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 aplicaciones SPA, y espero que despu茅s de leer esta publicaci贸n se sienta c贸modo usando estas tecnolog铆as para proteger sus aplicaciones.

    Como siempre, gracias por leer y no dude en comentar o criticar a continuaci贸n.

    Etiquetas:

    Deja una respuesta

    Tu direcci贸n de correo electr贸nico no ser谩 publicada. Los campos obligatorios est谩n marcados con *