微信小程序登录与获取手机号 (Python)

news2024/12/21 20:25:34

文章目录

  • 相关术语
  • 登录逻辑
  • 登录设计
  • 登录代码

相关术语

调用接口[wx.login()]获取登录凭证(code)。通过凭证进而换取用户登录态信息,包括用户在当前小程序的唯一标识(openid)、微信开放平台账号下的唯一标识(unionid,若当前小程序已绑定到微信开放平台账号)及本次登录的会话密钥(session_key)等。

临时登录凭证 code 只能使用一次。

如果开发者拥有多个移动应用、网站应用、和公众账号(包括小程序),可通过 UnionID 来区分用户的唯一性,因为只要是同一个微信开放平台账号下的移动应用、网站应用和公众账号(包括小程序),用户的 UnionID 是唯一的。换句话说,同一用户,对同一个微信开放平台下的不同应用,UnionID是相同的。

目前微信登录无法拿到微信昵称与头像,需要自己处理该逻辑。

登录逻辑

登录逻辑主要分两步:

首先这是对应的逻辑设计图:
在这里插入图片描述

首先由前端生成一个code,这个code 需要返回给后端,所以后端需要拿到一个必须得一个code 参数,

后端拿到这个code 需要去拿到对应小程序的三个appid,appsecret (这两个是小程序注册时提供),code(前端传参)去请求API:

GET https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code 

请求参数

属性类型必填说明
appidstring小程序 appId
secretstring小程序 appSecret
js_codestring登录时获取的 code,可通过wx.login获取
grant_typestring授权类型,此处只需填写 authorization_code

返回参数

属性类型说明
session_keystring会话密钥
unionidstring用户在开放平台的唯一标识符,若当前小程序已绑定到微信开放平台账号下会返回,详见 UnionID 机制说明。
errmsgstring错误信息
openidstring用户唯一标识
errcodeint32错误码

后端现在通过API拿到如上的参数,需要返回给前端对应的session_key,unionid(或者openid)这一步相当于前端使用 code 换取 openid、unionid、session_key 等信息。

现在如果需要通过这个信息拿到手机号还是需要进行第二步的处理。

前端拿到这个信息后,需要经过一系列操作返回给后端三个参数。

data.encryptedData, data.Iv, data.session_key

后端拿到这三个参数并可以解密手机号,该加密与解密思路如下:

AES对称加密算法,并且采用了 AES CBC(Cipher Block Chaining)模式。[后面会详细谈]

至此一个登录逻辑已经完成,但这里只是讲的是微信登录逻辑,下面进入到设计模块。

登录设计

任何一个登录模块都少不了数据库,所以这里还需要结合数据库JWT等进行讲述。

首先是后端设计的表,本次使用django 框架进行设计表,除了django自带的User表以外,需要再设计一个user_expand表,但由于可以做一个兼容,可以把三方登录信息在再单独做一个表出来,如下:

三方登陆信息表:

字段名类型说明
idint【主键】
user_idint【逻辑外键】关联用户扩展表id
platformstring用户来源
platform_unique_idstring新增字段,微信的就是openid,谷歌的就是用户的邮箱,其他的类似【三方登陆唯一标识】
ynbool

这里需要把三方登录单独拿出来,因为是一个用户可能有多个三方登录信息,所以需要要设计成一对多的形式。

登录逻辑如下(需要绑定手机号):

1.前端给后端一个code 2.后端通过API 获得openid 3.查找三方表,是否有这个openid ,如果有那么拿到对应的user_id,然后refresh = RefreshToken.for_user(user) 返回给前端一个access_token即可;如果没有查到这个openid,那么需要返回给前端一个信息就是该用户没有绑定过的信息,需要前端返回一个加密向量(以及后端会给前端一个openid)。 4. 后端拿到这个加密向量,解密出这个手机号信息后,把手机号信息以及openid插入到对应数据表中,返回给前端access_token 以表示绑定成功。

登录代码

首先是微信登录使用的一些工具函数:

class WeiXinMiniAppLogin:
    async def get_three_login_unique_id(self, param: dict):
        code = param.get("token")
        login_params = WeiXinMiniAppLoginParams()
        wx_info = await self.get_wx_miniapp_info(code, await login_params.get_params())  # 获取到wx_info 的信息
        logger.info(f"wx_info: {wx_info}")
        return wx_info

    @staticmethod
    def get_login_type():
        return UserSourceEnum.WEIXINMINIAPP.value

    @staticmethod
    async def get_wx_miniapp_info(code: str, login_params: dict):
        params = {
            "appid": login_params.get("appid", ""),
            "secret": login_params.get("secret", ""),
            "js_code": code,
            "grant_type": login_params.get("grant_type", ""),
        }
        jscode2session_url = login_params.get("jscode2session_url", "")
        try:
            async with aiohttp.ClientSession() as session:
                async with session.get(jscode2session_url, params=params, ssl=False) as response:
                    if response.status != 200:
                        logger.error(f"WeiXinMiniAppLogin.jscode2session HTTP error: {response.status}")
                        return None
                    a = await response.text()  # Debug
                    data = json.loads(a)
                    # print(response)
                    # return data
                    # data = await response.json()  # B端的写法是data = json.loads(response.text),异步优先使用这个方法
        except Exception as e:
            logger.error(f"WeiXinMiniAppLogin.jscode2session error for HTTP: {e}")
            return None

        error_code = data.get("errcode")
        if error_code:
            logger.error(f"WeiXinMiniAppLogin.jscode2session error_code: {data}")
            return None

        session_key = data.get("session_key")
        openid = data.get("openid")
        unionid = data.get("unionid", None)  # 注意为空的情况

        # 如果为空,记录下,本小程序都是空的情况
        if unionid is None:
            # logger.error(f"WeiXinMiniAppLogin.jscode2session error: missing unionid in {data}")
            r_data = {
                "session_key": session_key,
                "openid": openid
            }
            return r_data
        # 如果不空的话就返回,实际情况就是只有openid
        r_data = {
            "unionid": unionid,
            "session_key": session_key,
            "openid": openid
        }
        return r_data

    @staticmethod
    async def get_phone_number(encrypted_data, aes_iv, session_key):
        try:
            session_key = session_key.replace("\\", "")
            session_key_bytes = b64decode(session_key)
            aes_iv_bytes = b64decode(aes_iv)
            encrypted_data_bytes = b64decode(encrypted_data)
            cipher = AES.new(session_key_bytes, AES.MODE_CBC, aes_iv_bytes)
            decrypted_bytes = cipher.decrypt(encrypted_data_bytes)
            padding_len = decrypted_bytes[-1]
            decrypted_bytes = decrypted_bytes[:-padding_len]
            decrypted_str = decrypted_bytes.decode('utf-8')
            model = json.loads(decrypted_str)
            phone_number = model.get("phoneNumber", "")
            return phone_number
        except Exception as ex:
            logger.error(
                f"WeiXinMiniAppLogin.get_phone_number error,encrypted_data:{encrypted_data},aes_iv:{aes_iv},session_key:{session_key},errormsg:{ex}")
            return None

这里对get_phone_number的解密方法做一些说明:

这个函数使用的是 AES对称加密算法,并且采用了 AES CBC(Cipher Block Chaining)模式

具体分析:

  1. AES (Advanced Encryption Standard):一种对称加密算法,常用于保护数据的安全性。对称加密意味着加密和解密都使用相同的密钥(即这里的 session_key)。

  2. AES.MODE_CBC (Cipher Block Chaining 模式):这是AES的一种工作模式,CBC模式是将前一个加密块的密文与当前块进行异或后再进行加密。解密时也要通过相同的初始化向量(IV,即这里的 aes_iv)来解密。

  3. b64decode:表示输入的 session_key、aes_iv 和 encrypted_data 是通过 Base64 编码的,在解密之前需要将其从 Base64 格式解码成原始字节流。

  4. 填充和去除填充:AES加密的数据块必须是固定长度(一般为128位,也就是16字节),如果数据长度不够,就会自动添加填充字符。在解密后,代码通过 padding_len = decrypted_bytes[-1] 获取填充的长度,并通过 decrypted_bytes[:-padding_len] 去除填充。

  5. 最终数据解密:解密后的数据是一个 JSON 字符串,最后通过 json.loads(decrypted_str) 解析为字典对象,获取其中的 phoneNumber。

下面是登录接口的设计,注意需要构造两个接口,

首先是三方登录回调接口:

前端传入参数:
{
    "login_type": "string", # 可省略,做一个三方登录标识
    "token": "string" 
}
后端返回参数:
//需要绑定手机号
{
    "status": 200,
    "message": "OK",
    "data": {
        "third_login_unique_id": "wx_XXXXXXXXX",
        "need_binding": true, # 需要绑定手机号
        "session_key": "some_session_key"
    }
}
// 之前绑定手机号的
// {
//     "status": 200,
//     "message": "OK",
//     "data": {
//         "need_binding": false, 
//         "refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.",
//         "access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV"
//     }
// }

然后是绑定手机号的逻辑:

前端传入参数:
{
    "third_login_unique_id": "wx_onwXXXXXX",
    "encryptedData": "xxxx",
    "Iv": "xxxx",
    "session_key": "xxxx"
}
后端返回参数:
{
    "status": 200,
    "message": "OK",
    "data": {
        "refresh": "eyJhbGciOiJIUzI1N",
        "access": "eyJhbGciO"
    }
}
@api_controller('user/', tags=['login'], permissions=[])
class LoginController:

    @http_post('third_login/, response=ThirdLoginBResponse)
    async def third_login(self, data: ThirdLoginBRequest):
        # 获取登录类型
        login_type = data.login_type  # weixinminiapp
        third_login_instances = third_login_initializer.get_instance(login_type)
        if third_login_instances is None:
            raise HttpError(status_code=400, detail="third_login_instances is None")

        # 获取微信信息
        wx_info = await third_login_instances.get_three_login_unique_id({"token": data.token})
        if wx_info is None:
            raise HttpError(status_code=400, detail="three_login_unique_id is None")

        # 获取 openid 和 unionid
        openid = wx_info.get("openid")
        unionid = wx_info.get("unionid", None)
        logger.info(f"ThirdLoginBView.post openid:{openid},unionid:{unionid},login_type:{login_type}")

        old_third_login_unique_id = f"wx_{openid}"  # 小程序只有openid无法拿到unionid
        # new_third_login_unique_id = f"wx_{unionid}" if unionid else old_third_login_unique_id

        # 先从三方表里面去查这个用户
        user_social_account = await UserSocialAccount.objects.filter(
            platform_unique_id=old_third_login_unique_id,
            platform=login_type
        ).afirst()
        User = get_user_model()
        # 如果用户存在,直接返回对应的token值
        if user_social_account:
            user_id = user_social_account.user_id
            user = await User.objects.aget(id=user_id)
            refresh = RefreshToken.for_user(user)
            response_data = {
                "status": 200,
                "message": "OK",
                "data": {
                    "third_login_unique_id": old_third_login_unique_id,  # 只有openid
                    "need_binding": False,
                    "refresh": str(refresh),
                    "access": str(refresh.access_token),
                }
            }
            response_data["data"]["session_key"] = wx_info.get("session_key")  # 调试解密,该参数只有在需要绑定的时候返回,这里做个测试保留
        else:
            # 如果用户不存在,返回需要绑定的信息
            response_data = {
                "status": 200,
                "message": "OK",
                "data": {
                    "third_login_unique_id": old_third_login_unique_id,
                    "need_binding": True
                }
            }
            if login_type == UserSourceEnum.WEIXINMINIAPP.value:
                response_data["data"]["session_key"] = wx_info.get("session_key")

        return ThirdLoginBResponse(**response_data)

    @http_post('wx_binding_phone/', response=WXBindingPhoneResponse)
    async def wx_binding_phone(self, data: WXBindingPhoneRequest):
        # logger.info("ThirdLoginView.post start:" + json.dumps(data.dict(), ensure_ascii=False))

        # 解密获取手机号
        wx_mini_app_login = WeiXinMiniAppLogin()
        phone_number = await wx_mini_app_login.get_phone_number(data.encryptedData, data.Iv, data.session_key)
        if not phone_number:
            raise HttpError(status_code=400, detail="无法解密获取手机号")

        # 获取用户模型
        User = get_user_model()

        # 查找或创建用户

        user, created = await User.objects.aget_or_create(username=phone_number)
        if created:
            user.set_unusable_password()
            await user.asave()

        # 查找或创建用户扩展信息
        user_expand, created = await UserExpand.objects.aget_or_create(user=user)

        if created:
            user_expand.phone = user.username
            user_expand.nick_name = phone_number
            user_expand.avatar = "https://thirdwx.qlogo.cn/mmopen/vi_32/POgEwh4mIHO4nibH0KlMECNjjGxQUq24ZEaGT4poC6icRiccVGKSyXwibcPq4BWmiaIGuG1icwxaQX6grC9VemZoJ8rg/132"
            # 缺头像,暂时不处理,昵称就是手机号,目前无法拿到昵称和头像
            await user_expand.asave()
            # 创建新的 UserSocialAccount 对象并保存
            user_social_account = UserSocialAccount(
                user_id=user.id,
                platform=UserSourceEnum.WEIXINMINIAPP.value,
                platform_unique_id=data.third_login_unique_id,
            )
            await user_social_account.asave()
        else:
            
            pass
        refresh = RefreshToken.for_user(user)

        # 构建响应数据
        response_data = {
            "status": 200,
            "message": "OK",
            "data": {
                'refresh': str(refresh),
                'access': str(refresh.access_token),
            }
        }

        return WXBindingPhoneResponse(**response_data)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2119671.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

华为防火墙 nat64

如果设备接收到的IPv6报文的前缀是设备为NAT64定义的前缀,说明报文的目的地址是IPv4网络,报文将经过NAT64处理后被转发至IPv4网络。 如果设备接收到的IPv6报文的前缀不是设备为NAT64定义的前缀,说明报文的目的地址是IPv6网络,报文…

强烈推荐!分享5款ai论文生成软件

在当今学术研究和写作领域,AI论文生成工具的出现极大地提高了写作效率和质量。这些工具不仅能够帮助研究人员快速生成论文草稿,还能进行内容优化、查重和排版等操作。以下是五款值得推荐的AI论文生成软件,特别是千笔-AIPassPaper。 ### 千笔-…

Gin-封装自动路由

O.0 思路一、API二、控制层三、自动路由核心四、分组路由外加中间件使用 思路 由于Java转Go直接使用的goframe框架,然学习Gin时觉得一个接口一个路由太麻烦,于是有了...1、在请求结构体中采用标签的形式,直接给出路由和请求方式 2、在控制层…

yum源配置与静态配置地址

网络yum源 备份配置文件 下载新的CentOS-Base.repo文件到/etc/yum.repos.d/目录下 执行yum clean all清除原有 yum 缓存 执行yum makecache(刷新缓存) 本地yum 将/etc/yum/repos.d/下的文件a都移走,此处移到了该目录下的bak中 找到光盘路…

【重学 MySQL】二十二、limit 实现分页

【重学 MySQL】二十二、limit 实现分页 基本语法实现分页第一页第二页通用公式注意事项在 MySQL 中,LIMIT 子句非常强大,它允许你限制查询结果的数量,同时也经常被用来实现分页功能。分页是 Web 开发中常见的需求,它允许用户浏览大量数据时,一次只查看一小部分数据。 基本…

【重学 MySQL】二十一、order by 实现数据排序

【重学 MySQL】二十一、order by 实现数据排序 基本语法示例按薪水升序排序按薪水降序排序根据多个列排序 注意事项 在MySQL中,ORDER BY子句用于对结果集中的数据进行排序。你可以根据一个或多个列对结果进行升序(ASC)或降序(DESC…

JavaEE:文件操作

文章目录 文件操作和IO文件系统操作File介绍属性构造方法方法 代码演示前四个listmkdirrenameTo 文件操作和IO 文件系统操作 创建文件,删除文件,创建目录,重命名… Java中有一个类,可以帮我们完成上述操作. 这个类叫做File类. File介绍 属性 这个表格描述了文件路径的分隔符…

【IIS实战】ERR_SSL_KEY_USAGE_INCOMPATIBLE

当我们第一次配置IIS服务器做测试环境网站时,如果没有插手做自签名证书,而是用IIS自带的自签名证书,那么现代浏览器访问HTTPS测试站点大概率会有下图所示的报错: (IE:我能打开( •̀ ω •́ )y&#xff0…

VuePress搭建个人博客(手动安装)

天行健,君子以自强不息;地势坤,君子以厚德载物。 每个人都有惰性,但不断学习是好好生活的根本,共勉! 文章均为学习整理笔记,分享记录为主,如有错误请指正,共同学习进步。…

ENSP配置云服务找不到以太网卡【已解决】

在搭建网络拓扑图的时候,想要连接云,发现没有以太网卡 环境:Windows10,ensp模拟器 以为一直是用轻薄本,上网都是连接wifi,所以没用上以太网卡。 一、在电脑环境上安装以太网卡 winR跳出运行口&#xff0c…

chapter13-常用类——(StringBuffer StringBuilder)—day15

475-StringBuffer结构剖析 476-StringBuffer转换 477-StringBuffer方法

2024.9.9

优化登录框: 当用户点击取消按钮,弹出问题对话框,询问是否要确定退出登录,并提供两个按钮,yes|No,如果用户点击的Yes,则关闭对话框,如果用户点击的No,则继续登录 当用户…

Java后台生成二维码

一、效果图 二、实现代码 1.添加依赖 <!-- zxing生成二维码 --> <dependency><groupId>com.google.zxing</groupId><artifactId>core</artifactId><version>3.3.3</version> </dependency><dependency><grou…

【Dart 教程系列第 50 篇】在 Flutter 项目的国际化多语言中,如何根据翻译提供的多语言文档表格,快速生成不同语言的内容

这是【Dart 教程系列第 50 篇】&#xff0c;如果觉得有用的话&#xff0c;欢迎关注专栏。 博文当前所用 Flutter SDK&#xff1a;3.22.1、Dart SDK&#xff1a;3.4.1 文章目录 一&#xff1a;问题描述二&#xff1a;解决方案三&#xff1a;完整代码 一&#xff1a;问题描述 在…

学会分析问题,画出分析图,解释问题过程,找出规律 ;整数数组分为左右2个部分,左边位奇数右边偶数

// 整数数组左边是奇数右边是偶数.cpp : Defines the entry point for the console application. //#include "stdafx.h" #include<stdio.h> void swap(int& a,int& b) {int tempa;ab;btemp; } int main(int argc, char* argv[]) {int a[7]{1,2,3,4,5,…

使用jenkins 打包前端私服代码失败的问题

问题现象&#xff1a; jinekins 流水线在yarn 编译前端私服依赖包的时候&#xff0c;报错&#xff0c;提示 Permission denied (publickey,gssapi-keyex,gssapi-with-mic,password). 【emm。。。之前的构建都是好好的&#xff0c;也不知道前端大哥啥时候去封装的前端代码&am…

【每日刷题】Day115

【每日刷题】Day115 &#x1f955;个人主页&#xff1a;开敲&#x1f349; &#x1f525;所属专栏&#xff1a;每日刷题&#x1f34d; &#x1f33c;文章目录&#x1f33c; 1. LCR 089. 打家劫舍 - 力扣&#xff08;LeetCode&#xff09; 2. LCR 090. 打家劫舍 II - 力扣&…

阿里云服务器镜像,有大用处

大家好&#xff0c;我是小悟 有时候阿里云旧服务器快到期了&#xff0c;想把项目、数据、软件挪到新服务器上&#xff0c;如果全部重新搭建的话&#xff0c;那无疑是耗时又费力。有了镜像迁移&#xff0c;就方便了许多。 新旧服务器的类型要一致&#xff0c;比如都是ECS服务器…

Matlab程序练习

Part1 1.求 [100,999] 之间能被 21整除的数的个数。 程序&#xff1a; 主文件&#xff1a;main.m clear; start_num 100; end_num 999; div_num 21; res div(start_num,end_num,div_num); fprintf("[%d,%d]之间能被%d整除的数的个数为%d个\n",start_num,end_…

使用Azure+C#+visual studio开发图像目标检测系统

在这篇文章里面&#xff0c;我们讲解使用AzureC#visual studio在Azure上做图像的目标检测系统。 笔者是头一次接触C#。之前以Python Java和Scala为主。感觉C#.Net是一种挺好用的开发系统。C#和Java非常像。会一个学另一个很快。 首先&#xff0c;目标检测是个什么东西&#x…