本demo后端采用DRF框架(djangorestframeworf)框架,有对DRF不熟悉的可以先看看我之前的文章 分类 Django Restframework 下的文章

前端使用的是uni-app ,因为发现uni-app是在太好用了。就随便写了登录的例子

创建项目数据

安装DRF pip install djangorestframework 创建新项目并新建app为users 这里我们直接继承django自带的user模型

from django.db import models
from django.contrib.auth.models import AbstractUser
from shortuuidfield import ShortUUIDField
# Create your models here.

class UserProfile(AbstractUser):

    uid = ShortUUIDField(primary_key=True, verbose_name='用户表主键')
    openid = models.CharField(unique=True,max_length=200, blank=True, null=True, verbose_name='开放id')
    telephone = models.CharField(unique=True, max_length=11, null=True, verbose_name="手机号码")
    username = models.CharField(unique=True, max_length=20, null=True, verbose_name='用户名')
    nickname = models.CharField(max_length=128, null=True, verbose_name='昵称')
    avatar = models.CharField(max_length=200, verbose_name='头像链接', default='https://dmall.wouldmissyou.com/20200822112506.jpg')
    gender = models.CharField(max_length=20, default='未知', verbose_name="性别")
    province = models.CharField(max_length=128, verbose_name='所在地区', null=True)
    address = models.CharField(max_length=128, verbose_name='详细地址', null=True)
    is_active = models.BooleanField(default=True, verbose_name="是否可用")
    is_staff = models.BooleanField(default=False, verbose_name="是否是员工")
    date_joined = models.DateTimeField(auto_now_add=True, verbose_name='加入时间')

然后在setting.py文件制定我们的模型

#setting.py
AUTH_USER_MODEL = 'users.UserProfile'

然后进行数据迁移

序列化用户模型

在users app中,新建serializers.py文件进行模型序列化,我们要想前端返回我们的user数据,除了密码之外的我们都要返回,具体返回的内容还是看自己实际项目中的需求,我们就直接把密码排除点,其他的都返回

from rest_framework import serializers
from .models import UserProfile


class UserProfileSerializer(serializers.ModelSerializer):
    class Meta:
        model = UserProfile
        exclude = ['password']

使用jwt进行认证

DRF中内置了好几种认证方法,但都不是我们所需要的,我们这里要使用JWT的方式进行认证,所以先安装一个包

pip install pyjwt

然后在users APP中新建一个py文件作为我们处理认证的文件,authentications.py

#authentications.py
import jwt
import time
from django.conf import settings
from rest_framework.authentication import BaseAuthentication, get_authorization_header
from rest_framework import exceptions
from django.contrib.auth import get_user_model
from jwt.exceptions import ExpiredSignatureError

User = get_user_model()


def generate_jwt(user):
    expire_time = time.time() + 60 * 60 * 24 * 7
    return jwt.encode({"userid": user.pk, "exp": expire_time}, key=settings.SECRET_KEY).decode('utf-8')


class JWTAuthentication(BaseAuthentication):
    keyword = 'JWT'

    def authenticate(self, request):
        auth = get_authorization_header(request).split()

        if not auth or auth[0].lower() != self.keyword.lower().encode():
            return None

        if len(auth) == 1:
            msg = "不可用的JWT请求头!"
            raise exceptions.AuthenticationFailed(msg)
        elif len(auth) > 2:
            msg = '不可用的JWT请求头!JWT Token中间不应该有空格!'
            raise exceptions.AuthenticationFailed(msg)

        try:
            jwt_token = auth[1]
            jwt_info = jwt.decode(jwt_token, settings.SECRET_KEY)
            userid = jwt_info.get('userid')
            try:
                # 绑定当前user到request对象上
                user = User.objects.get(pk=userid)
                return user, jwt_token
            except:
                msg = '用户不存在!'
                raise exceptions.AuthenticationFailed(msg)
        except ExpiredSignatureError:
            msg = "JWT Token已过期!"
            raise exceptions.AuthenticationFailed(msg)

这个认证方式也是修改于DRF的内置方法BaseAuthentication,可以进源码看一下。

视图

关于微信认证的方式大家可以去看一下官方文档,这里就不阐述了 首先到微信小程序后台获取自己的appkey和appid 在setting.py中配置上

#setting.py
APP_ID = '填写你自己的小程序appid'
APP_KEY = '填写你自己的小程序appkey'

# 配置使用JWT方式认证
REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": ['users.authentications.JWTAuthentication']
}
#users/views.py
from rest_framework.views import APIView
from .serializers import UserProfileSerializer
from .authentications import generate_jwt
from rest_framework import status
from rest_framework.response import Response
from django.conf import settings
import requests, json
from django.contrib.auth import get_user_model
User = get_user_model()
from django.utils.timezone import now

# Create your views here.

class LoginView(APIView):
    def post(self, request):
        """进行code验证"""
        code = request.data.get('code')
        if not code:
            return Response({'message': "缺少code"}, status=status.HTTP_400_BAD_REQUEST)

        url = "https://api.weixin.qq.com/sns/jscode2session?appid={0}&secret={1}&js_code={2}&grant_type=authorization_code" \
            .format(settings.APP_ID, settings.APP_KEY, code)

        r = requests.get(url)
        res = json.loads(r.text)
        openid = res['openid'] if 'openid' in res else None

        if not openid:
            return Response({"message":"微信调用失败"}, status=status.HTTP_503_SERVICE_UNAVAILABLE)

        # 判断是否是第一次登陆
        try:
            user = User.objects.get(openid=openid)
            user.last_login = now()
            # print(user.last_login)
            user.save()

        except Exception:
            # 微信用户第一次登陆,新建用户
            nickname = request.data.get('nickName')
            gender = request.data.get('gender')
            avatar = request.data.get('avatarUrl')
            province = request.data.get('province')+request.data.get('city')
            user = User.objects.create(nickname=nickname, province=province, gender=gender, avatar=avatar, openid=openid,
                                       password=openid)

        serializer = UserProfileSerializer(user)
        token = generate_jwt(user)
        return Response({"user": serializer.data, "token": token, 'status':status.HTTP_202_ACCEPTED})

然后在配置路由

path('login/', views.LoginView.as_view(), name='login'),

至此,后台部分配置完毕

前端部分

我这边为了后期方便开发,直接封装了一个简单的request.js

const baseURL = 'http://127.0.0.1:8000/';
const http = (options) => {
    return new Promise((resolve, reject) => {
        uni.showLoading({
            title: '加载中...',
            mask: options.load || false // 默认遮罩出现可以继续操作
        });
        try{
            uni.request({
                url: (options.baseURL || baseURL) + options.url,
                method: options.method || 'POST', // 默认为POST请求
                data: options.data, //请求超时在manifest.json配置
                header: {
                    'token': uni.getStorageSync('token'),
                    'Content-Type': options.header == 'form' ? 'application/x-www-form-urlencoded' : 'application/json'
                },
                success: res => {
                    resolve(res.data)
                },
                fail: (err) => {
                    reject(err.data);
                    console.log(err);
                    uni.showToast({
                        title: '请检查网络连接',
                        icon: 'none'
                    })
                    /*错误码处理
                    let code = err.data.code; 
                    switch (code) {
                        case 1000:
                            break;
                        default:
                            break;
                    } */
                },
                complete: () => {
                    uni.hideLoading();
                }
            });
        }catch(e){
            uni.hideLoading();
            uni.showToast({
                title: '服务端异常',
                icon: 'none'
            })
        }

    })
}

export default http

然后在main.js中声明一下

import http from "@/utils/http.js"
Vue.prototype.$HTTP = http

然后编写Login.vue

<template>
    <view>
        <!-- #ifdef MP-WEIXIN -->
        <view>
            <view>
                <view class='header'>
                    <image :src='userInfo.avatar'></image>
                    <text>{{userInfo.nickname}}</text>
                </view>
                <view class='content'>
                    <view>申请获取以下权限</view>
                    <text>获得你的公开信息(昵称,头像、地区等)</text>
                </view>

                <button class='bottom' type='primary' open-type="getUserInfo" withCredentials="true" lang="zh_CN" @getuserinfo="wxGetUserInfo">
                    授权登录
                </button>
                <button class='bottom' type='warn' @click="logoutBtn">退出登录</button>

            </view>
        </view>
        <!-- #endif -->
    </view>
</template>

<script>
    export default {
        data() {
            return {
                userInfo: {},
                isLogin: false //默认为true
            };
        },
        mounted(){
            const getuserInfo = uni.getStorageSync("userInfo")
            this.userInfo = getuserInfo
        },
        methods: {
            wxGetUserInfo() {
                let _this = this;
                uni.getUserInfo({
                    provider: 'weixin',
                    lang:"zh_CN",
                    success: function(infoRes) {
                        _this.data = infoRes.userInfo
                        // console.log(_this.data)
                        uni.showLoading({
                            title: "登录中"
                        })
                        uni.login({
                            provider: "weixin",
                            success: (loginres) => {
                                if (!loginres.code) {
                                    console.log("登录失败,请再次点击~~");
                                    return;
                                }
                                _this.data['code'] = loginres.code;
                                const params = _this.data
                                // console.log(params)

                                _this.$HTTP({
                                    method: "POST",
                                    url: "login/",
                                    data: params
                                }).then((res) => {
                                    console.log(res)
                                    if (res.status=='202') {
                                        uni.setStorageSync('token', 'JWT '+res.token)
                                        uni.setStorageSync('userInfo', res.user)
                                        _this.userInfo = uni.getStorageSync('userInfo')

                                    }
                                })

                            }
                        })

                    }
                })
            },
            logoutBtn(){
                // 先判断用户是否登录
                const userStatus = uni.getStorageSync('token')
                if(!userStatus){
                    uni.showToast({
                        icon:"none",
                        title:"您未登录"
                    })
                }else{
                    uni.removeStorageSync('token')
                    this.userInfo = {}
                }
            }
        },
    }
</script>

<style>
    .header {
        padding: 90rpx;
        border-bottom: 1px solid #ccc;
        text-align: center;
        width: 100%;
        line-height: 60rpx;
        box-sizing: border-box;
        display: flex;
        align-items: center;
        justify-content: center;
        flex-direction: column;
    }

    .header image {
        width: 200rpx;
        height: 200rpx;
    }


    .content {
        margin-left: 50rpx;
        margin-bottom: 90rpx;
    }

    .content text {
        display: block;
        color: #9d9d9d;
        margin-top: 40rpx;
    }

    .bottom {
        border-radius: 80rpx;
        margin: 70rpx 50rpx;
        font-size: 35rpx;
    }
</style>

最后生成小程序看看效果吧

最后修改:2020年8月24日 09:51