用户模型类的创建
首先:Django中的认证系统提供了的用户模型类和方法很方便,我们可以使用这个模型类,对于没有的字段就需要额外添加。
Django提供的认证系统,可以同时处理认证和授权。
Django的认证系统包含有:
- 用户
- 权限:用二元标志来指示一个用户是否可以做一个特定的任务。
- 组:对多个用户运用标签和全新的一种通用的方式。
- 一个可配置的密码哈希系统
- 用户登录或内容显示的表单和视图
- 一个可插拔的后台系统
Django认证系统提供了用户模型类User保存用户的数据,默认的User包含一下常见的基本字段:
-
username:用户名
-
first_name:(可选)
-
last_name:(可选)
-
email:(可选)
-
password:密码(Django不保存原始密码)
-
is_staff:布尔值,指示用户是否可以访问Admin站点
-
date_joined:账户创建时间
-
last_login:用户最后一次登录的时间
常用的方法:
-
set_password(raw_password):设置用户的密码
-
check_password(raw_password):如果给定的raw_password是用户的真实密码,则放回True,可以在检验用户密码时使用。
开始创建
Django提供了django.contrib.auth.models.AbstractUser用户抽象模型类允许我们继承,扩展字段来使用Django认证系统的用户模型类。
所以
- 先导入AbstractUser,在创建用户模型类的时候继承它。
- 在模型类中添加额外需要的字段
- 在配置文件中进行设置,不然系统不知道按照认证系统的模型类去创建。
AUTH_USER_MODEL = 'users.User'
- 执行数据库迁移,注意:一定要在AUTH_USER_MODEL配置到后再迁移。
注册业务接口分析
接口设计的思路:
- 分析要实现的业务逻辑,明确在这个业务中需要涉及到几个相关子业务,将每个子业务当做一个接口来设计。
- 分析接口的功能任务,明确接口的访问方式与返回数据:
比如:
- 接口的请求方式,如GET 、POST 、PUT等
- 接口的URL路径定义
- 需要前端传递的数据及数据格式(如路径参数、查询字符串、请求体表单、JSON等)
- 返回给前端的数据及数据格式
分析在用户注册中,需要实现以下接口:
- 图片验证码
- 短信验证码
- 用户名判断是否存在
- 手机号判断是否存在
- 注册保存用户数据
其中图片验证码和短信验证码考虑后续业务也会用到,故创建一个新应用verifications,在此应用中实现图片验证码、短信验证码方便别处调用。
图片验证码
实现逻辑:
- 将验证码图片返回前端
- 将正确内容保存到redis中。
访问方式: GET /image_codes/(?P<image_code_id>[\w-]+)/
由于是get请求,将图片ID参数携带在请求url中。而ID的是由前端生成的。
这个项目使用的是第三方库,captcha:/卡谱洽/,用来生成图片验证码。
from rest_framework.views import APIView
from meiduo_mall.libs.captcha.captcha import captcha
from django_redis import get_redis_connection
class ImageCodeView(APIView):
"""
图片验证码
"""
def get(self, request, image_code_id):
"""
获取图片验证码
"""
# 生成验证码图片
text, image = captcha.generate_captcha()
redis_conn = get_redis_connection("verify_codes")
redis_conn.setex("img_%s" % image_code_id, constants.IMAGE_CODE_REDIS_EXPIRES, text)
# 固定返回验证码图片数据,不需要REST framework框架的Response帮助我们决定返回响应数据的格式
# 所以此处直接使用Django原生的HttpResponse即可
return HttpResponse(image, content_type="images/jpg")
# 最后不能使用Response返回图片,应为Response默认会使用render对返回的值进行渲染,它无法对图片进行渲染,故使用Django原生的HttpResponse进行返回(它可以接受图片)。
说明:(先要在配置用文件中配置好redis)django-redis提供了get_redis_connection的方法,通过调用get_redis_connection方法传递redis的配置名称可获取到redis的连接对象,通过redis连接对象可以执行redis命令。
短信验证码
有需要对数据的校验故使用序列化器较为简洁:
get_serializer 方法在创建序列化器对象时会自动补充context属性,context属性包含三个值:request(请求对象) 、format(请求数据格式) 、view(类视图对象)。
在django的类视图对象中,kwargs属性保存了从路径中提取出来的参数。
所以在序列化器中获取手机号可以用一下方式获取:
mobile = self.context['view'].kwargs['mobile']
发送短信使用第三方工具包:云通讯
视图代码:
def get(self, request, mobile):
# 校验参数 由序列化器完成
serializer = self.get_serializer(data=request.query_params)
serializer.is_valid(raise_exception=True)
# 生成短信验证码
sms_code = '%06d' % random.randint(0, 999999)
# 保存短信验证码 保存发送记录
redis_conn = get_redis_connection('verify_codes')
# redis_conn.setex("sms_%s" % mobile, constants.SMS_CODE_REDIS_EXPIRES, sms_code)
# redis_conn.setex("send_flag_%s" % mobile, constants.SEND_SMS_CODE_INTERVAL, 1)
# redis管道
pl = redis_conn.pipeline()
pl.setex("sms_%s" % mobile, constants.SMS_CODE_REDIS_EXPIRES, sms_code)
pl.setex("send_flag_%s" % mobile, constants.SEND_SMS_CODE_INTERVAL, 1)
# 让管道通知redis执行命令
pl.execute()
# 使用celery发送短信验证码
expires = constants.SMS_CODE_REDIS_EXPIRES // 60
send_sms_code.delay(mobile, sms_code, expires, constants.SMS_CODE_TEMP_ID)
return Response({'message': 'OK'})
其中代码优化:
使用redis管道来统一操作数据库,提高性能。
使用celery发送短信验证码,异步处理任务。
关于celery的异步实现参考下方链接:
celery异步实现短信验证码发送
其中短信验证时的跨域请求解决办法:
图片验证码是采用浏览器发送请求:
- 即将图片当做一种页面资源加载,使用 src=“不同的资源路径”
- 每当资源路径改变浏览器就会自动请求资源
短信验证码是采用CORS实现跨域请求:
CORS原理:
浏览器在实现跨域请求之前会发送一个option请求询问后端是否支持
后端提供option请求的支持,告诉浏览器,支持那些域名的访问
创建Django中间件 提供option请求
实现步骤:
- 安装:pip install django-cors-headers
- 添加应用
INSTALLED_APPS = (
...
'corsheaders',
...
)
- 中间层设置
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
...
]
- 添加白名单
# CORS
CORS_ORIGIN_WHITELIST = (
'127.0.0.1:8080',
'localhost:8080',
'www.meiduo.site:8080',
'api.meiduo.site:8000'
)
CORS_ALLOW_CREDENTIALS = True # 允许携带cookie
凡是出现在白名单中的域名,都可以访问后端接口
CORS_ALLOW_CREDENTIALS 指明在跨域访问中,后端是否支持对cookie的操作。
注册功能实现
在注册的后端代码中需要实现的逻辑有:
- 接受参数
- 校验参数
- 保存用户数据,密码加密
- 序列化,返回数据
这几步逻辑可以直接使用CreateAPIView来实现
class UserView(CreateAPIView):
"""
用户注册
传入参数:
username, password, password2, sms_code, mobile, allow
"""
serializer_class = serializers.CreateUserSerializer
序列化器中需要重写create方法,添加密码加密的方法
class CreateUserSerializer(serializers.ModelSerializer):
"""创建用户的序列化器"""
password2 = serializers.CharField(label='确认密码', write_only=True)
sms_code = serializers.CharField(label='短信验证码', write_only=True)
allow = serializers.CharField(label='同意协议', write_only=True)
token = serializers.CharField(label='JWT token', read_only=True)
class Meta:
model = User
fields = ('id', 'username', 'password', 'password2', 'sms_code', 'mobile', 'allow', 'token')
extra_kwargs = {
'username': {
'min_length': 5,
'max_length': 20,
'error_messages': {
'min_length': '仅允许5-20个字符的用户名',
'max_length': '仅允许5-20个字符的用户名',
}
},
'password': {
'write_only': True,
'min_length': 8,
'max_length': 20,
'error_messages': {
'min_length': '仅允许8-20个字符的密码',
'max_length': '仅允许8-20个字符的密码',
}
}
}
def validate_mobile(self, value):
"""验证手机号"""
if not re.match(r'^1[3-9]\d{9}$', value):
raise serializers.ValidationError('手机号格式错误')
return value
def validate_allow(self, value):
"""检验用户是否同意协议"""
if value != 'true':
raise serializers.ValidationError('请同意用户协议')
return value
def validate(self, data):
# 判断两次密码
if data['password'] != data['password2']:
raise serializers.ValidationError('两次密码不一致')
# 判断短信验证码
redis_conn = get_redis_connection('verify_codes')
mobile = data['mobile']
real_sms_code = redis_conn.get('sms_%s' % mobile)
if real_sms_code is None:
raise serializers.ValidationError('无效的短信验证码')
if data['sms_code'] != real_sms_code.decode():
raise serializers.ValidationError('短信验证码错误')
return data
def create(self, validated_data):
"""重写保存方法,增加密码加密"""
# 移除数据库模型类中不存在的属性
del validated_data['password2']
del validated_data['sms_code']
del validated_data['allow']
# user = User.objects.create(username=xxx, password=xx)
# user = User.objects.create(**validated_data)
user = super().create(validated_data)
user.set_password(validated_data['password']) # 这是Django认证系统中自带的加密方法
user.save()
# 签发jwt token
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
user.token = token
return user
由于传统的cookie和session的验证机制有些许不足之处本项目使用JWT认证机制,详细原理请点链接查看:https://blog.csdn.net/weixin_43269166/article/details/88381575
登录部分
在Django REST framework JWT提供了登录签发JWTde视图,可以直接使用。
使用时直接定义路由即可:
from rest_framework_jwt.views import obtain_jwt_token
urlpatterns = [
url(r'^authorizations/$', obtain_jwt_token),
]
但是默认返回的值仅有token,我们还需要在返回值汇总增加username和user_id。通过修改该视图的返回值可以完成我们的需求,故在users/utils.py中创建下列函数:
这个函数是路由在向前端返回的时候会自动调用的
def jwt_response_payload_handler(token, user=None, request=None):
"""
自定义jwt认证成功返回数据
"""
return {
'token': token,
'user_id': user.id,
'username': user.username
}
修改配置文件:指明修改的返回值的函数位置
# JWT
JWT_AUTH = {
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1),
'JWT_RESPONSE_PAYLOAD_HANDLER': 'users.utils.jwt_response_payload_handler',
}
如果需要修改登录认证的方式则可以修改Django认证系统的认证后端,需要继承django.contrib.auth.backends.ModelBackend,并重写authenticate方法。
并在配置文件中告知Django使用我们自定义的认证后端:
AUTHENTICATION_BACKENDS = [
'users.utils.UsernameMobileAuthBackend',
]