Aula 90 – Tratamento e Encerramento de Sessões de Usuário
Aula 90 – Tratamento e Encerramento de Sessões de Usuário
Voltar para página principal do blog
Todas as aulas desse curso
Aula 89 Aula 91
Redes Sociais:
Meus links de afiliados:
Hostinger
Digital Ocean
One.com
Melhore seu NETWORKING
https://digitalinnovation.one/
Participe de comunidades de desenvolvedores:
Fiquem a vontade para me adicionar ao linkedin.
E também para me seguir no https://github.com/toticavalcanti.
Código final da aula:
https://github.com/toticavalcanti
Quer aprender python3 de graça e com certificado? Acesse então:
https://workover.com.br/python-codigo-fluente
Canais do Youtube
Toti
Lofi Music Zone Beats
Backing Track / Play-Along
Código Fluente
Putz!
Vocal Techniques and Exercises
PIX para doações
Aula 90 – Tratamento e Encerramento de Sessões de Usuário
O que faremos agora é colocar tudo o que aprendemos com o Object Viewed em uma sessão de usuário.
Vamos criar o modelo de sessão do usuário e um sinal(signal), importar algumas coisas, etc.
Antes de iniciar as modificações, crie uma branch para trabalhar as mudanças da aula e abra o models do analytics.
No código abaixo temos as Importações
django_ecommerce/e_commerce/analytics/models.py
from django.conf import settings
from django.db import models
from django.db.models.signals import pre_save, post_save
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.contrib.sessions.models import Session
from accounts.signals import user_logged_in
from .signals import object_viewed_signal
from .utils import get_client_ip
User = settings.AUTH_USER_MODEL
class ObjectViewed(models.Model):
user = models.ForeignKey(User, blank=True, null=True, on_delete=models.CASCADE) # specific user, instance.id
ip_address = models.CharField(max_length=220, blank=True, null=True)
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) # Product, Order, Cart, Address...
object_id = models.PositiveIntegerField() # User id, Product id, Order id
content_object = GenericForeignKey('content_type', 'object_id')
timestamp = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"{self.content_object} viewed on {self.timestamp}"
class Meta:
ordering = ['-timestamp'] # most recent saved show up first
verbose_name = 'Object viewed'
verbose_name_plural = 'Objects viewed'
def object_viewed_receiver(sender, instance, request, *args, **kwargs):
c_type = ContentType.objects.get_for_model(sender) # instance.__class__
new_view_obj = ObjectViewed.objects.create(
user = request.user,
content_type = c_type,
object_id = instance.id,
ip_address = get_client_ip(request)
)
object_viewed_signal.connect(object_viewed_receiver)
class UserSession(models.Model):
user = models.ForeignKey(User, blank=True, null=True, on_delete=models.CASCADE) # specific user, instance.id
ip_address = models.CharField(max_length=220, blank=True, null=True)
session_key = models.CharField(max_length=100, blank=True, null=True)
timestamp = models.DateTimeField(auto_now_add=True)
active = models.BooleanField(default=True)
ended = models.BooleanField(default=False)
def user_logged_in_receiver(sender, instance, request, *args, **kwargs):
user = instance
ip_address = get_client_ip(request)
session_key = request.session.session_key
UserSession.objects.create(
user = user,
ip_address = ip_address,
session_key = session_key
)
user_logged_in.connect(user_logged_in_receiver)
Faça as Migration:
python manage.py makemigrations
python manage.py migrate
A próxima coisa a fazer é criar um signal para login do usuário.
Então vamos criar o signals.py na pasta accounts.
django_ecommerce/e_commerce/accounts/signals.py
from django.dispatch import Signal
user_logged_in = Signal()
Importe ele no view.py do accounts.
A linha from .signal import user_logged_in
importa o sinal user_logged_in
definido no signals do accounts.
Neste caso, o sinal user_logged_in
é destinado a ser emitido sempre que um usuário faz login com sucesso, permitindo que outras partes do aplicativo reajam a esse evento.
A expressão user_logged_in.send(sender=user.__class__, request=request, user=user)
é onde o sinal user_logged_in
é efetivamente emitido, ou “enviado”.
Vamos quebrar essa linha para entender melhor:
sender
: O remetente do sinal, que é a classe do objetouser
(user.__class__
). Isso informa aos receptores do sinal qual classe está enviando o sinal.instance
: O objetouser
que foi autenticado e fez login.request
: O objeto de solicitação (self.request
), que contém informações sobre a solicitação HTTP atual, como parâmetros GET, POST e outros metadados.
Em resumo, essa linha está emitindo um sinal indicando que um usuário fez login com sucesso. Ele envia informações sobre o usuário que fez login e a solicitação HTTP associada ao login.
Os receptores desse sinal podem então executar ações com base nessas informações, como registrar o evento de login, atualizar dados do usuário, iniciar uma sessão, entre outras possibilidades.
Essencialmente, essa linha de código permite que você execute lógica adicional em outras partes do seu aplicativo em resposta a um usuário fazendo login, sem ter que acoplar firmemente essa lógica à sua view de login.
Por exemplo, você pode ter um receiver que registra a hora do login, um que verifica se o perfil do usuário está completo, ou qualquer outra ação que você queira realizar automaticamente quando um usuário faz login.
django_ecommerce/e_commerce/accounts/views.py
from django.contrib.auth import authenticate, login, logout, get_user_model
from django.views.generic import CreateView, FormView, View
from django.http import HttpResponse
from django.shortcuts import render,redirect
from django.utils.http import url_has_allowed_host_and_scheme
from .forms import LoginForm, RegisterForm, GuestForm
from .models import GuestEmail
from .signals import user_logged_in
def guest_register_view(request):
form = GuestForm(request.POST or None)
context = {
"form": form
}
next_ = request.GET.get('next')
next_post = request.POST.get('next')
redirect_path = next_ or next_post or None
if form.is_valid():
email = form.cleaned_data.get("email")
new_guest_email = GuestEmail.objects.create(email=email)
request.session['guest_email_id'] = new_guest_email.id
if url_has_allowed_host_and_scheme(redirect_path, request.get_host()):
return redirect(redirect_path)
else:
return redirect("/register/")
return redirect("/register/")
class LoginView(FormView):
form_class = LoginForm
success_url = '/' # Redireciona para a raiz do projeto
template_name = 'accounts/login.html'
def form_valid(self, form):
email = form.cleaned_data.get("email")
password = form.cleaned_data.get("password")
user = authenticate(request=self.request, username=email, password=password)
if user is not None:
login(self.request, user)
user_logged_in.send(sender=user.__class__, instance=user, request=self.request)
try:
del self.request.session['guest_email_id']
except:
pass
return super(LoginView, self).form_valid(form)
# def login_page(request):
# form = LoginForm(request.POST or None)
# context = {
# "form": form
# }
# next_ = request.GET.get('next')
# next_post = request.POST.get('next')
# redirect_path = next_ or next_post or None
# if form.is_valid():
# username = form.cleaned_data.get("username")
# password = form.cleaned_data.get("password")
# user = authenticate(request, username=username, password=password)
# if user is not None:
# login(request, user)
# try:
# del request.session['guest_email_id']
# except:
# pass
# if url_has_allowed_host_and_scheme( redirect_path, request.get_host() ):
# return redirect( redirect_path )
# else:
# # Redireciona para uma página de sucesso.
# return redirect("/")
# else:
# #Retorna uma mensagem de erro de 'invalid login'.
# print("Login inválido")
# return render(request, "accounts/login.html", context)
class LogoutView(View):
template_name = 'accounts/logout.html'
def get(self, request, *args, **kwargs):
context = {
"content": "Você efetuou o logout com sucesso! :)"
}
logout(request)
return render(request, self.template_name, context)
# def logout_page(request):
# context = {
# "content": "Você efetuou o logout com sucesso! :)"
# }
# logout(request)
# return render(request, "accounts/logout.html", context)
class RegisterView(CreateView):
form_class = RegisterForm
template_name = 'accounts/register.html'
success_url = '/login/'
# User = get_user_model()
# def register_page(request):
# form = RegisterForm(request.POST or None)
# context = {
# "form": form
# }
# if form.is_valid():
# form.save()
# return render(request, "accounts/register.html", context)
Precisamos de um método para encerrar a sessão depois de salvar no banco.
django_ecommerce/e_commerce/analytics/admin.py
from django.contrib import admin
from .models import ObjectViewed
from .models import UserSession
admin.site.register(ObjectViewed)
admin.site.register(UserSession)
A linha admin.site.register(UserSession)
no arquivo admin.py
do seu aplicativo Django está registrando o modelo UserSession
no site de administração do Django.
Isso significa que o Django irá gerar automaticamente uma interface no painel de administração para que você possa visualizar, adicionar, editar e deletar instâncias do modelo UserSession
.
Certifique-se que o projeto esteja rodando e faça o login no painel admin do django e na parte de Analytics clique em UserSession e em UserSessionObject.
Veja que você tem uma session key no UserSessionObject.
Vamos explorar o session no shell do django.
Abra outro terminal para poder deixar o servidor rodando e digite:
python manage.py shell
Copie o número da session key do UserSessionObject e atribua a session_key.
>>>session_key = 'qus0xwjinskfitjdn8dhrnhcwhu9f2ds'
Importe o Session
>>>from django.contrib.sessions.models import Session
>>>Session.objects.get(pk=session_key )
>>>Session.objects.get(pk=session_key ).delete()
Agora dê um refresh na página e veja que você foi deslogado.
Essa é a forma como django gerencia as sessões, como ele cria e deleta as sessões.
O Django usa uma sessão consistente entre o frontend e o backend.
Isso significa que, se o mesmo usuário estiver logando no admin e na loja com as mesmas credenciais e a sessão ainda estiver ativa, ele permanecerá logado em ambos os lugares.
A sincronização de sessão entre o admin e o frontend acontece naturalmente com o Django, porque o Django usa um sistema de sessão centralizado.
Quando um usuário faz login em qualquer parte do site, seja no admin ou no frontend, o Django cria uma sessão para esse usuário, que é válida em todo o site, a menos que você tenha configurado algo para separar explicitamente as sessões entre o admin e o frontend.
Voltando ao código
O código em azul abaixo, é a lógica para encerrar sessões de usuário no Django, juntamente com a definição de receivers para os sinais post_save que são conectados aos modelos UserSession e User.
Vamos detalhar cada parte:
FORCE_SESSION_TO_ONE e FORCE_INACTIVE_USER_ENDSESSION
Essas duas linhas de código estão obtendo valores de configuração do arquivo settings.py do projeto Django, com opções de fallback caso essas configurações não estejam explicitamente definidas.
FORCE_SESSION_TO_ONE = getattr(settings, ‘FORCE_SESSION_TO_ONE’, False): Esta linha busca a configuração FORCE_SESSION_TO_ONE dentro do objeto settings do Django. O método getattr é usado para tentar obter o valor dessa configuração, caso não esteja definido em settings.py, o valor padrão False é utilizado. Se FORCE_SESSION_TO_ONE for True, indica que o sistema deve forçar apenas uma sessão ativa por usuário a qualquer momento, encerrando automaticamente sessões anteriores quando uma nova sessão é iniciada.
FORCE_INACTIVE_USER_ENDSESSION = getattr(settings, ‘FORCE_INACTIVE_USER_ENDSESSION’, False): De maneira semelhante, esta linha busca a configuração FORCE_INACTIVE_USER_ENDSESSION. Se for True, o sistema encerra automaticamente todas as sessões ativas de um usuário quando sua conta é marcada como inativa (is_active = False).
Essas configurações permitem controlar o comportamento das sessões de usuário no seu projeto Django de maneira flexível.
Você pode habilitá-las adicionando as seguintes linhas ao seu arquivo settings.py:
FORCE_SESSION_TO_ONE = True # Força apenas uma sessão por usuário
FORCE_INACTIVE_USER_ENDSESSION = True # Encerra sessões quando o usuário é marcado como inativo
No settings.py vamos colocar como False ambas as opções, o código já tá controlando isso.
Método end_session()
O método end_session
é um método de instância, o que significa que ele é chamado em um objeto específico de UserSession
. Vamos passar por cada linha:
session_key = self.session_key
: Isso pega a chave da sessão (um identificador único para cada sessão) do objetoUserSession
atual.try:
: Este bloco é usado para “tentar” executar um código que pode potencialmente falhar, ou seja, pode lançar uma exceção.Session.objects.get(pk=session_key).delete()
: Aqui, tentamos encontrar uma sessão do Django (não umaUserSession
, mas a sessão real usada pelo Django para manter o estado de login) que tenha a chave primária (pk
) igual àsession_key
do nossoUserSession
. Se encontrada, a sessão é deletada, o que efetivamente desloga o usuário do ponto de vista do Django.
except Session.DoesNotExist:
: Se a sessão com asession_key
dada não existir, o Django lançará umaSession.DoesNotExist
exceção. Neste caso, usamospass
para simplesmente ignorar esse erro e continuar. Isso significa que se a sessão do Django já foi excluída, não faremos nada.except Exception as e:
: Se ocorrer qualquer outro tipo de exceção ao tentar excluir a sessão, capturamos essa exceção e a imprimimos no console. Isso é para fins de depuração e deve ser manuseado com cuidado em um ambiente de produção, pois a impressão de erros pode não ser a melhor maneira de lidar com exceções inesperadas.self.active = False
: Independente de a sessão do Django existir ou não, definimos o campoactive
do nosso objetoUserSession
comoFalse
, indicando que estaUserSession
não deve mais ser considerada ativa.self.ended = True
: Também definimos o campoended
comoTrue
, indicando que esta sessão de usuário foi encerrada.self.save()
: Finalmente, salvamos o objetoUserSession
com as atualizações que fizemos nos camposactive
eended
.
Método save()
O método save
é o método padrão do Django que é chamado quando você salva um objeto no banco de dados.
if not self.active and not self.ended:
: Antes de salvar o objeto, verificamos se o campoactive
éFalse
e o campoended
éFalse
. Se ambos foremFalse
, chamamos o métodoend_session
que acabamos de descrever. Isso pode ser usado para garantir que, se por algum motivo estivermos salvando uma sessão que deveria ser encerrada, encerramos corretamente.super().save(*args, **kwargs)
: Depois de lidar com o estado da sessão, chamamos o métodosave
da superclasse (no caso, osave
padrão do Django para modelos) para continuar o processo normal de salvar o objeto no banco de dados. Os*args
e**kwargs
são argumentos e argumentos nomeados que podem ser passados para o métodosave
padrão do Django e são aqui repassados.
django_ecommerce/e_commerce/analytics/models.py
from django.conf import settings
from django.db import models
from django.db.models.signals import pre_save, post_save
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.contrib.sessions.models import Session
from accounts.signals import user_logged_in
from .signals import object_viewed_signal
from .utils import get_client_ip
User = settings.AUTH_USER_MODEL
FORCE_SESSION_TO_ONE = getattr(settings, 'FORCE_SESSION_TO_ONE', False)
FORCE_INACTIVE_USER_ENDSESSION= getattr(settings, 'FORCE_INACTIVE_USER_ENDSESSION', False)
class ObjectViewed(models.Model):
user = models.ForeignKey(User, blank=True, null=True, on_delete=models.CASCADE) # specific user, instance.id
ip_address = models.CharField(max_length=220, blank=True, null=True)
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) # Product, Order, Cart, Address...
object_id = models.PositiveIntegerField() # User id, Product id, Order id
content_object = GenericForeignKey('content_type', 'object_id')
timestamp = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"{self.content_object} viewed on {self.timestamp}"
class Meta:
ordering = ['-timestamp'] # most recent saved show up first
verbose_name = 'Object viewed'
verbose_name_plural = 'Objects viewed'
def object_viewed_receiver(sender, instance, request, *args, **kwargs):
c_type = ContentType.objects.get_for_model(sender) # instance.__class__
new_view_obj = ObjectViewed.objects.create(
user = request.user,
content_type = c_type,
object_id = instance.id,
ip_address = get_client_ip(request)
)
object_viewed_signal.connect(object_viewed_receiver)
class UserSession(models.Model):
user = models.ForeignKey(User, blank=True, null=True, on_delete=models.CASCADE) # specific user, instance.id
ip_address = models.CharField(max_length=220, blank=True, null=True)
session_key = models.CharField(max_length=100, blank=True, null=True)
timestamp = models.DateTimeField(auto_now_add=True)
active = models.BooleanField(default=True)
ended = models.BooleanField(default=False)
def end_session(self):
session_key = self.session_key
try:
Session.objects.get(pk=session_key).delete()
except Session.DoesNotExist:
pass
except Exception as e:
print(f"Unexpected error ending session: {e}")
self.active = False
self.ended = True
self.save()
def save(self, *args, **kwargs):
# Se 'active' for False e 'ended' ainda não for True, encerra a sessão
if not self.active and not self.ended:
self.end_session()
super().save(*args, **kwargs)
def post_save_session_receiver(sender, instance, created, *args, **kwargs):
if created:
qs = UserSession.objects.filter(user=instance.user, ended=False, active=False).exclude(id=instance.id)
for i in qs:
i.end_session()
if not instance.active and not instance.ended:
instance.end_session()
if FORCE_SESSION_TO_ONE:
post_save.connect(post_save_session_receiver, sender=UserSession)
def post_save_user_changed_receiver(sender, instance, created, *args, **kwargs):
if not created:
if instance.is_active == False:
qs = UserSession.objects.filter(user=instance.user, ended=False, active=False)
for i in qs:
i.end_session()
if FORCE_INACTIVE_USER_ENDSESSION:
post_save.connect(post_save_user_changed_receiver, sender=User)
def user_logged_in_receiver(sender, instance, request, *args, **kwargs):
user = instance
ip_address = get_client_ip(request)
session_key = request.session.session_key
UserSession.objects.create(
user=user,
ip_address=ip_address,
session_key=session_key
)
user_logged_in.connect(user_logged_in_receiver)
django_ecommerce/e_commerce/e_commerce/settings.py
"""
Django settings for e_commerce project.
Generated by 'django-admin startproject' using Django 2.1.4.
For more information on this file, see
https://docs.djangoproject.com/en/2.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/2.1/ref/settings/
"""
import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'xjmv-0^l__duq4-xp54m94bsf02lx4&1xka_ykd_(7(5#9^1o^'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
#our apps
'addresses',
'analytics',
'billing',
'accounts',
'carts',
'orders',
'products',
'search',
'tags',
]
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
AUTH_USER_MODEL = 'accounts.User' # changes the built-in user model to ours
FORCE_SESSION_TO_ONE = False
FORCE_INACTIVE_USER_ENDSESSION= False
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
LOGOUT_REDIRECT_URL = '/login/'
ROOT_URLCONF = 'e_commerce.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'e_commerce.wsgi.application'
# Database
# https://docs.djangoproject.com/en/2.1/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
# Password validation
# https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/2.1/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.1/howto/static-files/
STATIC_URL = '/static/'
STATICFILES_DIRS = [
os.path.join(BASE_DIR, "static_local")
]
STATIC_ROOT = os.path.join(os.path.dirname(BASE_DIR), "static_cdn", "static_root")
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(os.path.dirname(BASE_DIR), "static_cdn", "media_root")
Deixar essas configurações como False
no settings.py
é uma abordagem deliberada que oferece várias vantagens.
Ao definir FORCE_SESSION_TO_ONE
e FORCE_INACTIVE_USER_ENDSESSION
como False
por padrão, o desenvolvedor ou administrador do sistema tem a flexibilidade para ativar essas funcionalidades apenas se e quando necessário. Isso permite uma personalização mais granular do comportamento do aplicativo sem modificar o código fonte, apenas alterando a configuração no settings.py
.
Deixar FORCE_SESSION_TO_ONE
e FORCE_INACTIVE_USER_ENDSESSION
como False
no settings.py
é uma prática que permite uma maior flexibilidade e minimiza surpresas para os desenvolvedores e administradores do sistema.
Essa abordagem reflete uma consideração cuidadosa dos princípios de design de software, onde mudanças significativas no comportamento do aplicativo devem ser opcionais, em vez de impostas por padrão.
Vamos testar
Queremos permitir multiplas sessões de usuário, se a gente quiser apenas uma sessão por usuário realmente mais a frente, será através do código que iremos controlar isso, e não configurando no settings.py.
Abra o ecommerce: localhost:8000/ e faça o login no ecommerce.
Abra o painel admin: localhost:8000/admin e faça o login também.
Abra uma janela anônima e tente fazer o login com o mesmo usuário.
Veja que ele agora não vai desconectar o login em uma das duas janelas, ele vai permitir mais de uma sessão aberta por usuário.
Agora desmarque o active da última sessão criada, e dê um refresh onde você logou por último, veja que agora você foi deslogado.