用 Django 开发微信小程序后端实现用户登录

本文将介绍采用 Django 开发微信小程序后端,通过将用户模块进行重构,并采用JWT来进行用户认证,来解决以下问题:

  1. 微信小程序不支持 Cookie,因此不能采用 Django 默认的 Session 验证机制;
  2. 同时小程序也不支持 Django 内置的用户登录模块。

希望通过此文可以帮助大家快速搭建小程序的后端服务。

1. 安装相关的依赖

在 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 进行安装

2. 用户模块重构

Django 默认的用户模块是 django.contrib.auth.models 内的AbstractUser,在完成以下配置以前不要对数据进行 migrate,否则会导致用户模块重构失败。

假设我们将重构的用户模块放置在 wx 应用内,命名为 WxUser

2.1. 创建 wx 应用

python3 manage.py startapp wx

2.2. settings.py 配置

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),
}

2.3. 在 wx 模块的 models.py 加入自定义的用户模块

对 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

2.4. 在 wx 模块的 admin.py 对 UserAdmin 进行自定义

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 的用户模块进行了重构。

3. 用户登录模块

3.1 配置 serializers

在 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']

3.2. 配置 views

在 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)

3.3. 配置允许跨域的 MIDDLEWARE

在 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

3.4. 配置 urls

在 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 参数。

4. 接口登录调用

在 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()
						}
					})
					
				})
				})
			}
		}
	};

4. 接口开发

基于 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()
    
  • 17
    点赞
  • 95
    收藏
    觉得还不错? 一键收藏
  • 11
    评论
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值