网站配色方法,如何建设一个个人网站,公众号开发制作,html学校网站模板目录 用户模型建立账号密码登录手机号登录验证码双后端鉴权JWT 介绍 用户模型建立
在django中自带auth_user表,字段名有id, password,username#xff0c;is_superuer,is_activate , email #xff0c;is_staff#xff08;用于标识某个用户是否可以登录到 Django 的管理界面… 目录 用户模型建立账号密码登录手机号登录验证码双后端鉴权JWT 介绍 用户模型建立
在django中自带auth_user表,字段名有id, password,usernameis_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, ADMINOperator 2, T2Merchant 3, T3class StatusSource(models.IntegerChoices):Inactive 1, 未激活Active 2, 已激活Deactivated 3, 已停用objects models.Manager()user models.OneToOneField(User, on_deletemodels.DO_NOTHING, primary_keyTrue, verbose_name用户)telephone models.CharField(max_length100, nullTrue, blankTrue, verbose_name用户手机号)company_alias models.CharField(max_length100, nullTrue, blankTrue, verbose_name企业简称)user_maker models.IntegerField(choicesUserMakerSource.choices, defaultUserMakerSource.Merchant,verbose_name用户端标记符) company_id models.IntegerField(nullTrue, blankTrue, verbose_name企业id)create_time models.DateTimeField(auto_now_addTrue, verbose_name创建时间)update_time models.DateTimeField(auto_nowTrue, verbose_name修改时间)status models.IntegerField(choicesStatusSource.choices, defaultStatusSource.Inactive, verbose_name用户状态)yn models.BooleanField(defaultTrue)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_deletemodels.DO_NOTHING, primary_keyTrue, verbose_name用户)#OneToOneField(User): 这表示 UserExpand 模型与 User 模型之间有一个一对一的关系。一对一字段的工作方式类似于外键区别在于它保证与之关联的另一个模型这里是 User是唯一的。也就是说每个 User 实例在 UserExpand 中只能有一个对应的实例反之亦然。# on_deletemodels.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(sourceuser.id, read_onlyTrue) # 获取关联用户的 IDcreate_time serializers.DateTimeField(format%Y-%m-%d, read_onlyTrue)update_time serializers.DateTimeField(format%Y-%m-%d, read_onlyTrue)class Meta:model UserExpandfields [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, senderUser)
#receiver(post_save, senderUser): 这是一个装饰器用于将下面定义的函数注册为一个信号接收器。post_save 是 Django 发送的一个信号每当任何数据库中的模型实例保存后都会发送这个信号。senderUser 参数指定这个接收器只响应 User 模型实例的保存操作。
def create_user_expand(sender, instance, created, **kwargs):if created:UserExpand.objects.create(userinstance, user_makerUserExpand.UserMakerSource.Operator)
在这里也需要对auth_user进行DRF 序列化操作
class UserSerializer(serializers.ModelSerializer):class Meta:model Userfields [id, username, password]extra_kwargs {password: {write_only: True}}def create(self, validated_data):user User.objects.create_user(**validated_data)return userclass UserListSerializer(serializers.ModelSerializer):class Meta:model Userfields [id, username]账号密码登录
只要是登录就不设置鉴权 鉴权是前端请求头 Authorization ‘Bearer eyJhXXXXXXXXX’
from rest_framework.permissions import AllowAnyclass 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, usernameusername, passwordpassword)if user is not None:# 登录成功处理 UserExpanduser_expand, created UserExpand.objects.get_or_create(useruser,defaults{user_maker: UserExpand.UserMakerSource.Operator, # adminstatus: UserExpand.StatusSource.Active, # 已激活})# 如果用户已存在检查并更新相关字段if not created:user_expand.status UserExpand.StatusSource.Active # 确保状态是已激活user_expand.save()# 生成 JWT tokenrefresh RefreshToken.for_user(user) # 创建 refresh 和 access tokensreturn Response({refresh: str(refresh),access: str(refresh.access_token),}, statusstatus.HTTP_200_OK)else:raise BusinessException(status_code.LOGIN_CODE_ERROR_40111, Invalid username or password.)在使用 Django REST frameworkDRF及其简单 JWTSimple 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({…}, statusstatus.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(days30), # 访问令牌30天有效REFRESH_TOKEN_LIFETIME: timedelta(days30), # 刷新令牌30天有效# 可以添加更多的配置项如签名算法、响应负载等
}
在apps:rest_framework, # RESTful APIrest_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_keyTrue)telephone models.CharField(max_length20, verbose_name手机号)code models.CharField(max_length100, verbose_name验证码)create_time models.DateTimeField(auto_now_addTrue, verbose_name创建时间)update_time models.DateTimeField(auto_nowTrue, verbose_name修改时间)yn models.BooleanField(defaultTrue)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(minutes10)return self.update_time ten_minutes_agoclass LoginCodeSerializer(serializers.ModelSerializer):class Meta:model LoginCodefields [id, telephone, code, create_time, update_time, yn] # 包括所有字段下面提供了一种代码思路
logger logging.getLogger(__name__)# 发送验证码线程池
SEND_CODE_THREADPOOL ThreadPoolExecutor(max_workers1, thread_name_prefixsend_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(fGetLoginCodeView 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, statusstatus.HTTP_200_OK)# 检查用户是否存在如果不存在则创建def check_user(self, username):检查用户是否存在如果不存在则创建并检查状态user, created self.User.objects.get_or_create(usernameusername)if created:user.set_unusable_password()user.save()user_expand, created UserExpand.objects.get_or_create(useruser)if created:user_expand.telephone usernameif 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.Activeuser_expand.save()return user_expanddef generate_code(self, destination):校验验证码生成时间 如果小于一分钟不生成未生成过验证码或之前生成时间大于一分钟重新生成ArgsReturns: 生成的验证码# 生成6位随机数字验证码code .join(random.sample(string.digits, 6))# 获取或创建验证码login_code, created LoginCode.objects.get_or_create(telephonedestination)# 如果验证码已经存在且创建时间在1分钟之内则不重新生成否则重新生成并更新验证码if not created:send_time login_code.update_timeone_minutes_ago timezone.now() - timedelta(minutes1)if send_time one_minutes_ago:code Noneelse:login_code.set_codes(code)login_code.save()# 如果验证码不存在则生成验证码else:login_code.set_codes(code)login_code.save()return codedef 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_IDreq.SignName settings.TENCENT_SMS_SIGN_NAMEreq.TemplateId settings.TENCENT_SMS_TEMPLATE_IDreq.TemplateParamSet [code, expiration_date]req.PhoneNumberSet [phone]resp client.SendSms(req)logger.info(resp.to_json_string(indent2))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(telephonedestination).first()if not login_code:return Response(status_data.LOGIN_CODE_ERROR_40111, statusstatus.HTTP_406_NOT_ACCEPTABLE)if not login_code.check_code(code):return Response(status_data.LOGIN_CODE_ERROR_40111, statusstatus.HTTP_406_NOT_ACCEPTABLE)User get_user_model()user User.objects.get(usernamedestination)try:userExpand UserExpand.objects.get(useruser)userExpand.save()except UserExpand.DoesNotExist:passrefresh RefreshToken.for_user(user)r_data {refresh: str(refresh),access: str(refresh.access_token),}return Response(r_data, statusstatus.HTTP_200_OK)
在处理短信验证码的发送过程中使用线程池如 ThreadPoolExecutor主要是出于性能和响应时间的考虑。以下是几个关键的原因和好处
异步操作 发送短信通常涉及网络请求这是一个可能耗时的操作尤其是当短信服务商的API响应时间不稳定或较慢时。如果在主执行线程通常是处理Web请求的线程中直接进行这类操作会导致整个Web请求的处理时间显著延长从而影响用户体验。不阻塞主线程 通过使用线程池来异步发送短信可以确保主执行线程快速响应用户请求例如立即返回一个“验证码已发送请查收”的消息。与此同时实际的短信发送操作在后台的另一个线程中处理从而不会阻塞主线程。资源管理和效率 线程池还有助于更有效地管理系统资源。它允许控制同时运行的线程数量避免了创建和销毁线程的高开销以及可能由于过多并发线程而导致的资源耗尽问题。max_workers1 意味着线程池中只有一个工作线程处理发送任务这可能是为了防止对短信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 asynciofrom utils.logger import logger
from utils.http_utils import aio_get, aio_postclass EchoiceCoreSDK:def __init__(self, base_urlhttp://localhost:8000):self.base_url base_urlasync 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: fBearer {token}})user_info response.get(data, {})return user_infoexcept Exception as e:logger.warning(fcheck_auth error: {e})return {}async def _post(self, path, **kwargs):url self.base_url pathkwargs.setdefault(headers, {})self.update_headers(kwargs[headers])status, resp_headers, resp_body await aio_post(url, **kwargs)if status ! 200:raise Exception(fFailed to get {url}, {resp_body})return resp_bodystaticmethoddef update_headers(headers: dict):headers[Content-Type] application/jsonheaders[Accept] application/jsonif 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 介绍
JWTJSON 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因其简单和扩展性已经成为现代网络应用中身份验证和授权的流行方式。