django 身份验证与鉴权
目录
- 用户模型建立
- 账号密码登录
- 手机号登录验证码
- 双后端鉴权
- JWT 介绍
用户模型建立
在django中自带auth_user表,字段名有id, password,username,is_superuer,is_activate , email ,is_staff(用于标识某个用户是否可以登录到 Django 的管理界面。如果 is_staff 设置为 True,该用户就可以访问管理后台;)等
(图片来源网络,侵删)可以用
python manage.py createsuperuser
来创建超级用户,然后输入 /admin 可进入后台添加用户,这里是使用账号和密码登录的权限。
但是由于django自带的auth_user表字段过少,所以这里使用用户拓展表的概念(方便日后对用户的字段进行扩展和数据迁移),如果继承auth_user 则很难做到扩展功能了。
所以一般建立user_expand模型进行,如图所示建立模型:
class UserExpand(models.Model): class UserMakerSource(models.IntegerChoices): ADMIN = 1, 'ADMIN' Operator = 2, 'T2' Merchant = 3, 'T3' class StatusSource(models.IntegerChoices): Inactive = 1, '未激活' Active = 2, '已激活' Deactivated = 3, '已停用' objects = models.Manager() user = models.OneToOneField(User, on_delete=models.DO_NOTHING, primary_key=True, verbose_name="用户") telephone = models.CharField(max_length=100, null=True, blank=True, verbose_name="用户手机号") company_alias = models.CharField(max_length=100, null=True, blank=True, verbose_name="企业简称") user_maker = models.IntegerField(choices=UserMakerSource.choices, default=UserMakerSource.Merchant, verbose_name="用户端标记符") company_id = models.IntegerField(null=True, blank=True, verbose_name="企业id") create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") update_time = models.DateTimeField(auto_now=True, verbose_name="修改时间") status = models.IntegerField(choices=StatusSource.choices, default=StatusSource.Inactive, verbose_name="用户状态") yn = models.BooleanField(default=True) class Meta: verbose_name = '2.用户扩展信息管理' verbose_name_plural = verbose_nameclass Meta 是一个内嵌的类,用于定义一些与模型相关的配置(非字段属性),不会作为数据库表的一部分存储。这些配置包括模型的一些行为特征和属性,例如:
verbose_name 和 verbose_name_plural:用来定义在 Django 管理后台显示的名字。verbose_name 是单数形式,表示一个模型实例的名称;verbose_name_plural 是复数形式,用来表示模型的名称集合。
在这里使用:
user = models.OneToOneField(User, on_delete=models.DO_NOTHING, primary_key=True, verbose_name="用户") #OneToOneField(User): 这表示 UserExpand 模型与 User 模型之间有一个一对一的关系。一对一字段的工作方式类似于外键,区别在于它保证与之关联的另一个模型(这里是 User)是唯一的。也就是说,每个 User 实例在 UserExpand 中只能有一个对应的实例,反之亦然。 # on_delete=models.DO_NOTHING: 这是一个用于指定当关联的 User 对象被删除时应如何处理 UserExpand 实例的参数。models.DO_NOTHING 表示如果删除了 User 对象,不会对 UserExpand 实例做任何操作。这种设置需要小心使用,因为它可能导致数据库中的外键引用不一致。(所以一般用假删除)在 UserExpand 模型中定义了一个名为 user 的字段,这个字段是一个一对一关系字段,连接到 Django 的内置用户模型 User。
由于前后端,一般以JSON传输,所以这里使用DRF 序列化进行操作。(用于将 UserExpand 模型的实例转换为 JSON 格式,并在 API 中进行交互。)
注意,要保证time是 只读,不可以进行修改!,同时进行了格式化处理,方便前端传入yy-mm-dd的形式。
class UserExpandModelSerializer(serializers.ModelSerializer): #这种序列化器直接与一个 Django 模型关联,自动处理模型字段到 JSON 数据的转换。 user_id = serializers.IntegerField(source='user.id', read_only=True) # 获取关联用户的 ID create_time = serializers.DateTimeField(format="%Y-%m-%d", read_only=True) update_time = serializers.DateTimeField(format="%Y-%m-%d", read_only=True) class Meta: model = UserExpand fields = ['user_id', 'telephone', 'company_alias', 'user_maker', 'company_id', 'create_time', 'update_time', 'status', 'yn'] read_only_fields = ('create_time', 'update_time')建立了user_expand与auth_user联系了,但是如果直接从auth_user插入数据不会写进user_expand,怎么办,在user_expand.models 写,层级独立。
@receiver(post_save, sender=User) #@receiver(post_save, sender=User): 这是一个装饰器,用于将下面定义的函数注册为一个信号接收器。post_save 是 Django 发送的一个信号,每当任何数据库中的模型实例保存后都会发送这个信号。sender=User 参数指定这个接收器只响应 User 模型实例的保存操作。 def create_user_expand(sender, instance, created, **kwargs): if created: UserExpand.objects.create(user=instance, user_maker=UserExpand.UserMakerSource.Operator)在这里也需要对auth_user进行DRF 序列化操作:
class UserSerializer(serializers.ModelSerializer): class Meta: model = User fields = ['id', 'username', 'password'] extra_kwargs = {'password': {'write_only': True}} def create(self, validated_data): user = User.objects.create_user(**validated_data) return user class UserListSerializer(serializers.ModelSerializer): class Meta: model = User fields = ["id", "username"]账号密码登录
只要是登录,就不设置鉴权
鉴权是前端请求头:
Authorization :‘Bearer eyJhXXXXXXXXX’
from rest_framework.permissions import AllowAny class SpecialistPasswordLoginView(views.APIView): permission_classes = [AllowAny] # 允许所有用户访问这个登录界面(后台管理,其余一律用token验证) def post(self, request): username = request.data.get("username") password = request.data.get("password") if not username or not password: raise BusinessException.build_by_dict(status_data.PARAM_ERROR_40001) user = authenticate(request, username=username, password=password) if user is not None: # 登录成功,处理 UserExpand user_expand, created = UserExpand.objects.get_or_create( user=user, defaults={ 'user_maker': UserExpand.UserMakerSource.Operator, # admin 'status': UserExpand.StatusSource.Active, # 已激活 } ) # 如果用户已存在,检查并更新相关字段 if not created: user_expand.status = UserExpand.StatusSource.Active # 确保状态是已激活 user_expand.save() # 生成 JWT token refresh = RefreshToken.for_user(user) # 创建 refresh 和 access tokens return Response({ "refresh": str(refresh), "access": str(refresh.access_token), }, status=status.HTTP_200_OK) else: raise BusinessException(status_code.LOGIN_CODE_ERROR_40111, "Invalid username or password.")在使用 Django REST framework(DRF)及其简单 JWT(Simple JWT)扩展来生成和返回 JSON Web Tokens (JWTs),其中包括一个刷新令牌(refresh token)和访问令牌(access token)。
refresh = RefreshToken.for_user(user): 这行代码通过调用 RefreshToken.for_user() 方法为指定的用户(user)创建一个新的刷新令牌。这个方法是 django-rest-framework-simplejwt 包提供的,用于为给定的用户生成一个新的 JWT 刷新令牌。刷新令牌主要用于在访问令牌过期后获取新的访问令牌,而无需用户重新登录。
return Response({…}, status=status.HTTP_200_OK): 这行代码创建并返回一个 HTTP 响应,状态码为 200 OK。响应的内容是一个包含两个键值对的字典:
“refresh”: str(refresh): 将刷新令牌转换为字符串格式并作为 refresh 键的值。
“access”: str(refresh.access_token): 通过访问刷新令牌的 access_token 属性获取对应的访问令牌,并将其转换为字符串格式作为 access 键的值。
authenticate() 函数的主要功能是检查用户提供的用户名和密码是否与数据库中存储的凭证匹配。如果匹配,它会返回对应的用户对象;如果不匹配,它会返回 None。
注意需要在setting.py设置:
SIMPLE_JWT = { "ACCESS_TOKEN_LIFETIME": timedelta(days=30), # 访问令牌30天有效 "REFRESH_TOKEN_LIFETIME": timedelta(days=30), # 刷新令牌30天有效 # 可以添加更多的配置项,如签名算法、响应负载等 } 在apps: "rest_framework", # RESTful API "rest_framework_simplejwt", # JWT 认证手机号登录验证码
手机登录验证码较为复杂,我总结了一下步骤:
用户输入手机号 , 后端拿到手机号并根据时间戳+手机号生成对应的随机6位验证码,然后将手机号和验证码(加密)存到数据库中(并设置对应的令牌时间),后端服务器将验证码和手机号发送到短信服务商中,短信服务商获取到对应的信息后,将验证码发送到对应的手机号中,用户收到验证码后,在页面输入验证码,后端在数据库查询手机号+验证码是否正确,如果正确和上述账号密码登录一样返回对应的token。
所以这里设计到三个model:
1.auth_user 2.user_expand 3.LoginCode
这是logincode 模型:这里的code是加密保存,使用了django自带的make_password
class LoginCode(models.Model): """ 登录验证码 """ objects = models.Manager() id = models.AutoField(primary_key=True) telephone = models.CharField(max_length=20, verbose_name="手机号") code = models.CharField(max_length=100, verbose_name="验证码") create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") update_time = models.DateTimeField(auto_now=True, verbose_name="修改时间") yn = models.BooleanField(default=True) def __str__(self): return f"{self.telephone} - {self.code}" def set_codes(self, raw_code): self.code = make_password(raw_code) def check_code(self, raw_code): return self.is_expired() and check_password(raw_code, self.code) def is_expired(self): ten_minutes_ago = timezone.now() - timedelta(minutes=10) return self.update_time >= ten_minutes_ago class LoginCodeSerializer(serializers.ModelSerializer): class Meta: model = LoginCode fields = ['id', 'telephone', 'code', 'create_time', 'update_time', 'yn'] # 包括所有字段下面提供了一种代码思路,
logger = logging.getLogger(__name__) # 发送验证码线程池 SEND_CODE_THREADPOOL = ThreadPoolExecutor(max_workers=1, thread_name_prefix="send_code_") class GetLoginCodeView(views.APIView): """ 获取登录验证码 """ User = get_user_model() permission_classes = [AllowAny] def post(self, request): # 手机号 destination = request.data.get("telephone") self.check_user(destination) code = self.generate_code(destination) if code is None: raise BusinessException.build_by_dict(status_data.LOGIN_CODE_ERROR_40112) logger.info(f"GetLoginCodeView destination:{destination},code:{code}") try: self.send_sms_code(destination, code) except Exception as e: raise BusinessException.build_by_dict(status_data.PARAM_ERROR_40001) return Response(status_data.OK_200, status=status.HTTP_200_OK) # 检查用户是否存在,如果不存在则创建 def check_user(self, username): """ 检查用户是否存在,如果不存在则创建,并检查状态 """ user, created = self.User.objects.get_or_create(username=username) if created: user.set_unusable_password() user.save() user_expand, created = UserExpand.objects.get_or_create(user=user) if created: user_expand.telephone = username if user_expand.status == UserExpand.StatusSource.Deactivated: raise BusinessException(status_code.LOGIN_CODE_ERROR_40111, "账户已被停用") if user_expand.status == UserExpand.StatusSource.Inactive: user_expand.status = UserExpand.StatusSource.Active user_expand.save() return user_expand def generate_code(self, destination): """ 校验验证码生成时间 如果小于一分钟,不生成 未生成过验证码或之前生成时间大于一分钟重新生成 Args Returns: 生成的验证码 """ # 生成6位随机数字验证码 code = ''.join(random.sample(string.digits, 6)) # 获取或创建验证码 login_code, created = LoginCode.objects.get_or_create(telephone=destination) # 如果验证码已经存在,且创建时间在1分钟之内,则不重新生成,否则重新生成并更新验证码 if not created: send_time = login_code.update_time one_minutes_ago = timezone.now() - timedelta(minutes=1) if send_time > one_minutes_ago: code = None else: login_code.set_codes(code) login_code.save() # 如果验证码不存在,则生成验证码 else: login_code.set_codes(code) login_code.save() return code def send_sms_code(self, phone, code): """ 发送短信验证码 Args: phone: 手机号 code: 验证码 Returns: """ SEND_CODE_THREADPOOL.submit(self.tencent_cloud_send_sms_code(phone, code, "10")) def tencent_cloud_send_sms_code(self, phone, code, expiration_date): """ 腾讯云发送短信验证码 Args: phone: 手机号 code: 验证码 expiration_date: 验证码有效期 Returns: """ try: cred = credential.Credential(settings.TENCENT_SMS_SECRET_ID, settings.TENCENT_SMS_SECRET_KEY) client = sms_client.SmsClient(cred, settings.TENCENT_SMS_REGION) req = models.SendSmsRequest() req.SmsSdkAppId = settings.TENCENT_SMS_APP_ID req.SignName = settings.TENCENT_SMS_SIGN_NAME req.TemplateId = settings.TENCENT_SMS_TEMPLATE_ID req.TemplateParamSet = [code, expiration_date] req.PhoneNumberSet = [phone] resp = client.SendSms(req) logger.info(resp.to_json_string(indent=2)) except TencentCloudSDKException as err: logger.error("tencent send sms code error:", err) class CheckLoginCodeView(views.APIView): """ 检查登录验证码 """ permission_classes = [AllowAny] def post(self, request): destination = request.data.get("telephone") code = request.data.get("code") login_code = LoginCode.objects.filter(telephone=destination).first() if not login_code: return Response(status_data.LOGIN_CODE_ERROR_40111, status=status.HTTP_406_NOT_ACCEPTABLE) if not login_code.check_code(code): return Response(status_data.LOGIN_CODE_ERROR_40111, status=status.HTTP_406_NOT_ACCEPTABLE) User = get_user_model() user = User.objects.get(username=destination) try: userExpand = UserExpand.objects.get(user=user) userExpand.save() except UserExpand.DoesNotExist: pass refresh = RefreshToken.for_user(user) r_data = { 'refresh': str(refresh), 'access': str(refresh.access_token), } return Response(r_data, status=status.HTTP_200_OK)在处理短信验证码的发送过程中使用线程池,如 ThreadPoolExecutor,主要是出于性能和响应时间的考虑。以下是几个关键的原因和好处:
- 异步操作:
发送短信通常涉及网络请求,这是一个可能耗时的操作,尤其是当短信服务商的API响应时间不稳定或较慢时。如果在主执行线程(通常是处理Web请求的线程)中直接进行这类操作,会导致整个Web请求的处理时间显著延长,从而影响用户体验。
- 不阻塞主线程:
通过使用线程池来异步发送短信,可以确保主执行线程快速响应用户请求,例如立即返回一个“验证码已发送,请查收”的消息。与此同时,实际的短信发送操作在后台的另一个线程中处理,从而不会阻塞主线程。
- 资源管理和效率:
线程池还有助于更有效地管理系统资源。它允许控制同时运行的线程数量,避免了创建和销毁线程的高开销,以及可能由于过多并发线程而导致的资源耗尽问题。max_workers=1 意味着线程池中只有一个工作线程处理发送任务,这可能是为了防止对短信API的过度请求或控制成本。
- 可扩展性和控制:
使用线程池还提供了更好的扩展性和控制。如果未来业务增长,需要更高的并发处理能力,可以简单地调整 max_workers 参数来增加处理能力。同时,线程池提供的是一种更结构化的方式来处理并发任务,使得代码更易于管理和维护。
- 异常处理和稳定性:
在独立的线程中处理短信发送还允许更好地隔离和管理可能出现的异常,不会影响主要的业务流程。此外,可以在这些后台线程中实现更复杂的错误处理逻辑,如重试机制等。
- 日志记录:
你的代码中也显示了日志记录器的设置 logger = logging.getLogger(name),这是为了在线程池中执行的任务出现问题时能够记录详细的日志信息,从而便于调试和追踪问题。
总之,使用 ThreadPoolExecutor 是一种提高Web应用性能和用户响应速度的常见实践,尤其是在涉及外部网络请求的操作中。这种方法可以有效地改善系统的可用性和响应性,同时保持代码的清晰和易于维护。
注意使用腾讯短信服务商服务,需要在settings.py设置:
TENCENT_SMS_SECRET_ID = '' TENCENT_SMS_SECRET_KEY = '' TENCENT_SMS_REGION = '' TENCENT_SMS_ENDPOINT = 'sms.tencentcloudapi.com' TENCENT_SMS_APP_ID = '' TENCENT_SMS_SIGN_NAME = '' TENCENT_SMS_TEMPLATE_ID = ''
双后端鉴权
一般来说很多项目不止一个后端来运行,比如针对于对话项目,一个后端使用django,一个后端使用websockets协议,django对用户设置了权限了,但是websockets如何鉴权呢?
这里提供一个思路。
首先在django服务中写入一个得到用户信息的接口:POST /user/info/,请求头参数Authorization
django返回响应:
{ "status": 200, "message": "OK", "data": { "id":1, "telephone": "1234456", "company_id": 8, "status":1, "user_maker":1, } }所以ws服务只要请求这个接口,然后拿到这个telephone就表示这个用户是对的了。一般ws请求参数写出token = ,所以访问ws服务的时候,ws首先会提取到这个用户token,扔给django获取用户信息是否正确。
import asyncio from utils.logger import logger from utils.http_utils import aio_get, aio_post class EchoiceCoreSDK: def __init__(self, base_url="http://localhost:8000"): self.base_url = base_url async def check_auth(self, token: str) -> dict: """检查用户token是否有效 :param token: 用户token :return: { "status": 200, "message": "OK", "data": { "id":1, "telephone": "1234456", "company_id": 8, "status":1, "user_maker":1, } } """ try: response = await self._post( "/user/info/", headers={"Authorization": f"Bearer {token}"} ) user_info = response.get("data", {}) return user_info except Exception as e: logger.warning(f"check_auth error: {e}") return {} async def _post(self, path, **kwargs): url = self.base_url + path kwargs.setdefault("headers", {}) self.update_headers(kwargs["headers"]) status, resp_headers, resp_body = await aio_post(url, **kwargs) if status != 200: raise Exception(f"Failed to get {url}, {resp_body=}") return resp_body @staticmethod def update_headers(headers: dict): headers["Content-Type"] = "application/json" headers["Accept"] = "application/json" if headers.get("Authorization") == "Bearer ": headers.pop("Authorization") ce_core_sdk = ceCoreSDK() async def main(): sdk = ceCoreSDK() token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." user_info = await sdk.check_auth(token) print("User Info:", user_info) if __name__ == "__main__": asyncio.run(main())JWT 介绍
JWT(JSON Web Tokens)是一种开放标准(RFC 7519),用于在网络应用环境间安全地传递声明(claims)。它被广泛用于身份验证和信息交换。JWT设计轻巧,可以通过URL、POST参数或在HTTP头内传输。它主要由三个部分组成,用点(.)连接成一个长字符串。
可以不用在服务端存储认证信息(比如 token),完全由客户端提供,服务端只要根据 JWT 自身提供的解密算法就可以验证用户合法性,而且这个过程是安全的。
组成:头部、载荷与签名
头部:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
{ "alg": "HS256", "typ": "JWT" } # 对应base64编码载荷
用来存储服务器需要的数据,比如用户信息,例如姓名、性别、年龄等,要注意的是重要的机密信息最好不要放到这里,比如密码等。
{ "name": "古时的风筝", "introduce": "英俊潇洒" }签名有一个计算公式。
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), Secret )
使用HMACSHA256算法计算得出,这个方法有两个参数,前一个参数是 (base64 编码的头部 + base64 编码的载荷)用点号相连,后一个参数是自定义的字符串密钥,密钥不要暴露在客户端,近应该服务器知道。
JWT最常用于:
身份验证:用户登录后,服务器生成一个JWT并返回给客户端,客户端后续请求将JWT包含在Header中。服务器通过验证JWT来认证用户身份。
信息交换:JWT安全地在各方之间传递信息,确保信息可以验证和可信。
JWT因其简单和扩展性,已经成为现代网络应用中身份验证和授权的流行方式。
- 异步操作:
