本文将介绍采用 Django 开发微信小程序后端,通过将用户模块进行重构,并采用JWT来进行用户认证,来解决以下问题:
希望通过此文可以帮助大家快速搭建小程序的后端服务。
在 requirements.txt 里面添加以下内容
certifi==2019.11.28
chardet==3.0.4
Click==7.0
coreapi==2.3.3
coreapi-cli==1.0.9
coreschema==0.0.4
Django==2.2.10
django-filter==2.2.0
djangorestframework==3.11.0
djangorestframework-simplejwt==4.4.0
idna==2.9
itypes==1.1.0
Jinja2==2.11.1
Markdown==3.2.1
MarkupSafe==1.1.1
Pillow==7.0.0
psycopg2-binary==2.8.4
pyasn1==0.4.8
pycryptodome==3.9.6
Pygments==2.5.2
PyJWT==1.7.1
python-weixin==0.5.0
pytz==2019.3
requests==2.23.0
rsa==4.0
simplejson==3.17.0
six==1.14.0
sqlparse==0.3.0
uritemplate==3.0.1
urllib3==1.25.8
xmltodict==0.12.0
然后运行 pip install -r requirements.txt 进行安装
Django 默认的用户模块是 django.contrib.auth.models 内的AbstractUser,在完成以下配置以前不要对数据进行 migrate,否则会导致用户模块重构失败。
假设我们将重构的用户模块放置在 wx 应用内,命名为 WxUser。
python3 manage.py startapp wx
在 INSTALLED_APPS 里面注册 wx 应用,通过 AUTH_USER_MODEL 指定用户模块。
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
...,
'rest_framework',
'django_filters',
'wx',
]
# 微信小程序 AppID 和 AppSecret
WX_APP_ID = '***'
WX_APP_SECRET = '*****'
# 指定用户模块
AUTH_USER_MODEL = 'wx.WxUser'
MIDDLEWARE = [
'wx.middlewares.MiddlewareHead', # 允许进行跨域访问
'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',
]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework_simplejwt.authentication.JWTAuthentication', # 通过 JWT 进行用户验证,验证过程需要访问数据库
'rest_framework_simplejwt.authentication.JWTTokenUserAuthentication', # 通过 JWT 的 Token 进行用户验证,验证过程不需要访问数据库
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
'PAGE_SIZE': 10,
'DEFAULT_FILTER_BACKENDS': (
'rest_framework.filters.SearchFilter',
'django_filters.rest_framework.DjangoFilterBackend',
'rest_framework.filters.OrderingFilter'
),
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema'
}
JWT_SIGNING_KEY = '******'
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(days=15),
'REFRESH_TOKEN_LIFETIME': timedelta(days=15),
'ROTATE_REFRESH_TOKENS': False,
'BLACKLIST_AFTER_ROTATION': True,
'ALGORITHM': 'HS256',
'SIGNING_KEY': JWT_SIGNING_KEY,
'VERIFYING_KEY': None,
'AUTH_HEADER_TYPES': ('Bearer',),
'USER_ID_FIELD': 'id',
'USER_ID_CLAIM': 'user_id',
'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
'TOKEN_TYPE_CLAIM': 'token_type',
'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp',
'SLIDING_TOKEN_LIFETIME': timedelta(days=15),
'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=15),
}
对 AbstractUser 进行重写
import hashlib
from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.utils.translation import gettext_lazy as _
class WxUser(AbstractUser):
"""
自定义的用户模块
"""
# 微信同步的用户信息
openid = models.CharField(
verbose_name=_('微信OpenID'), help_text=_('微信OpenID'), max_length=100, unique=True, null=True, blank=True)
avatar_url = models.URLField(
verbose_name=_('头像'), help_text=_('头像'), null=True, blank=True)
nick_name = models.CharField(
verbose_name=_('昵称'), help_text=_('昵称'), max_length=100, null=True, blank=True, unique=True)
gender = models.SmallIntegerField(
verbose_name=_('性别'), help_text=_('性别'), choices=((1, '男'), (2, '女'), (0, '未知')), null=True, blank=True)
language = models.CharField(
verbose_name=_('语言'), help_text=_('语言'), max_length=100, null=True, blank=True)
city = models.CharField(
verbose_name=_('城市'), help_text=_('城市'), max_length=200, null=True, blank=True)
province = models.CharField(
verbose_name=_('省份'), help_text=_('省份'), max_length=200, null=True, blank=True)
country = models.CharField(
verbose_name=_('国家'), help_text=_('国家'), max_length=200, null=True, blank=True)
date_of_birth = models.DateField(verbose_name=_('出生日期'), help_text=_('出生日期'), null=True, blank=True)
desc = models.TextField(verbose_name=_('描述'), help_text=_('描述'), max_length=2000, null=True, blank=True)
def create_username_password(self):
if not self.username and not self.password and self.openid:
key = settings.SECRET_KEY
self.username = hashlib.pbkdf2_hmac(
"sha256", getattr(self, 'openid').encode(encoding='utf-8'), key.encode(encoding='utf-8'), 10).hex()
self.password = hashlib.pbkdf2_hmac(
"sha256", self.username.encode(), getattr(self, 'openid').encode(encoding='utf-8'), 10).hex()
def save(self, *args, **kwargs):
self.create_username_password()
super().save(*args, **kwargs)
class Meta(AbstractUser.Meta):
swappable = 'AUTH_USER_MODEL'
create_username_password() 方法将自动通过openid创建用户名 username 和密码 password。
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.utils.translation import gettext_lazy as _
from wx.models import *
@admin.register(WxUser)
class WxUserAdmin(UserAdmin):
readonly_fields = (
'last_login', 'date_joined',
'nick_name', 'city', 'province', 'country', 'china_district', 'avatar_url'
)
search_fields = [
'username', 'openid', 'email', 'first_name', 'last_name', 'nick_name'
]
fieldsets = (
(_('基础信息'), {'fields': ('username', 'password', 'openid')}),
(_('个人信息'), {'fields': (
'nick_name', 'first_name', 'last_name', 'avatar_url', 'gender', 'date_of_birth', 'desc')}),
(_('联络信息'), {'fields': ('email',)}),
(_('地址信息'), {'fields': ('city', 'province', 'country', 'china_district')}),
(_('登录信息'), {'fields': ('last_login', 'date_joined')}),
)
通过以上步骤,我们已经将 Django 的用户模块进行了重构。
在 wx 模块下面添加 serializers.py
from rest_framework import serializers
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from .models import *
class JfwTokenObtainPairSerializer(TokenObtainPairSerializer):
@classmethod
def get_token(cls, user):
token = super(JfwTokenObtainPairSerializer, cls).get_token(user)
token['username'] = 'wx_{0}'.format(user.username)
return token
class WxUserSerializer(serializers.ModelSerializer):
class Meta:
model = WxUser
fields = ['id', 'nick_name', 'avatar_url', 'gender']
在 wx 模块添加 views.py
import json
import logging
from django.forms import model_to_dict
from rest_framework.response import Response
from rest_framework.status import *
from rest_framework.views import APIView
from weixin import WXAPPAPI
from weixin.oauth2 import OAuth2AuthExchangeError
from .serializers import *
logger = logging.getLogger('django')
def create_or_update_user_info(openid, user_info):
"""
创建或者更新用户信息
:param openid: 微信 openid
:param user_info: 微信用户信息
:return: 返回用户对象
"""
if openid:
if user_info:
user, created = WxUser.objects.update_or_create(openid=openid, defaults=user_info)
else:
user, created = WxUser.objects.get_or_create(openid=openid)
return user
return None
class WxLoginView(APIView):
"""
post:
微信登录接口
"""
authentication_classes = []
permission_classes = []
fields = {
'nick_name': 'nickName',
'gender': 'gender',
'language': 'language',
'city': 'city',
'province': 'province',
'country': 'country',
'avatar_url': 'avatarUrl',
}
def post(self, request):
user_info = dict()
code = request.data.get('code')
logger.info("Code: {0}".format(code))
user_info_raw = request.data.get('user_info', {})
if isinstance(user_info_raw, str):
user_info_raw = json.loads(user_info_raw)
if not isinstance(user_info_raw, dict):
user_info_raw = {}
logger.info("user_info: {0}".format(user_info_raw))
if code:
api = WXAPPAPI(appid=settings.WX_APP_ID, app_secret=settings.WX_APP_SECRET)
try:
session_info = api.exchange_code_for_session_key(code=code)
except OAuth2AuthExchangeError:
session_info = None
if session_info:
openid = session_info.get('openid', None)
if openid:
if user_info_raw:
for k, v in self.fields.items():
user_info[k] = user_info_raw.get(v)
user = create_or_update_user_info(openid, user_info)
if user:
token = JfwTokenObtainPairSerializer.get_token(user).access_token
return Response(
{
'jwt': str(token),
'user': model_to_dict(
user,
fields=[
'company', 'restaurant', 'current_role',
'is_owner', 'is_client', 'is_manager'
])
},
status=HTTP_200_OK)
return Response({'jwt': None, 'user': {}}, status=HTTP_204_NO_CONTENT)
在 wx 模块下面添加 middlewares.py
from django.utils.deprecation import MiddlewareMixin
class MiddlewareHead(MiddlewareMixin):
@staticmethod
def process_response(request, response):
if request:
response['Access-Control-Allow-Origin'] = '*'
return response
在 wx 模块下面添加 urls.py
from django.urls import re_path
from rest_framework.documentation import include_docs_urls
from rest_framework.urlpatterns import format_suffix_patterns
from wx.apps import WxConfig
from .views import *
API_TITLE = 'API Documents'
API_DESCRIPTION = 'API Information'
app_name = WxConfig.name
urlpatterns = format_suffix_patterns([
re_path(r'^wx_login/$', WxLoginView.as_view(), name='wx_login'),
# 其他接口
])
在项目目录的 urls.py 添加以下内容
from django.conf import settings
from django.contrib import admin
from django.views.static import serve
from django.urls import re_path, include
from rest_framework.documentation import include_docs_urls
API_TITLE = 'API Documents'
API_DESCRIPTION = 'API Information'
urlpatterns = [
re_path(r'^admin/', admin.site.urls),
re_path(r'^api/', include('wx.urls', namespace='api')),
re_path(r'^api-auth/', include('rest_framework.urls')),
re_path(r'^docs/', include_docs_urls(title=API_TITLE, description=API_DESCRIPTION)),
]
用户登录的接口已经配置完成,请求的时候通过 POST 方法传递 code 和 user_info 参数。
在 uniapp 里面可以用以下方式进行调用
export default {
data() {
return {
logining: false
};
},
onLoad() {},
methods: {
wxLogin(e) {
const that = this;
that.logining = true;
let userInfo = e.detail.userInfo;
uni.login({
provider:"weixin",
success:(login_res => {
let code = login_res.code;
uni.getUserInfo({
success(info_res) {
console.log(info_res)
console.log(urls.login)
uni.request({
url: 'your.domain/api/wx_login/',
method:"POST",
header: {'content-type': 'application/x-www-form-urlencoded'},
data:{
code : code,
user_info : info_res.rawData
},
success(res) {
if(res.statusCode == 200){
console.log(' 登录成功')
that.$store.commit('login',userInfo);
// uni.setStorageSync("userInfo",userInfo);
// uni.setStorageSync("skey", res.data.data);
}else{
console.log('登录失败')
console.log(res)
}
},
fail(error) {
console.log(error)
}
})
uni.hideLoading()
uni.navigateBack()
}
})
})
})
}
}
};
基于 rest_framework.generics 里面的 ListAPIView, RetrieveAPIView, GenericAPIView, RetrieveUpdateAPIView, CreateAPIView, ListCreateAPIView, RetrieveUpdateDestroyAPIView 可以快速开发接口。
from rest_framework.generics import ListAPIView, RetrieveAPIView, GenericAPIView, RetrieveUpdateAPIView, \
CreateAPIView, ListCreateAPIView, RetrieveUpdateDestroyAPIView
class GameTokenView(GenericAPIView):
"""
:get 获取用户当前的思维点
:post 添加和减少思维点
"""
serializer_class = GameTokenSerializer
def get(self, request):
user_id = request.user.id # 从 request中直接获取用户
data = get_current_game_point(user_id)
return Response(data)
def post(self, request):
user_id = request.user.id
raw_data = request.data
raw_data['user_id'] = user_id
serializer = self.get_serializer(data=raw_data)
try:
serializer.is_valid(raise_exception=True)
serializer.create(serializer.validated_data)
status = 200
except ValidationError:
status = 400
data = get_current_game_point(user_id)
return Response(data=data, status=status)
如果有的接口开发时,你不想走默认的权限配置。你可以自己定义里面的 authentication_classes 和 permission_classes。
class PublicGenericAPIView(GenericAPIView):
authentication_classes = () # 在此重新定义认证方式
permission_classes = () # 在此重新定义权限
def get(self, request):
return Response()
def post(self, request):
return Response()