【GoLang】记录一次使用Go实现微信小程序一键登录操作

需求

PC端一个B/S应用,登陆时支持微信扫码登陆,微信扫码后,会跳转到一个小程序,通过该小程序进行授权登陆,用小程序的原因是只有小程序能获取到用户的手机号

实现

整体流程

  • 首先页面需要生成一个二维码:后端需要提供一个唯一的code,使用uuid即可,然后再拼接为小程序页面的url,二维码由前端根据url生成即可
  • 将code存入缓存,设置状态为:1-初始状态
  • 前端不断扫描该code的状态,用以更新二维码状态(一直持续到登陆成功)
  • 当用户扫描二维码后,进入小程序,并调用后端接口,将code状态改为:2-已扫描
  • 小程序需要调用wx.login接口,获取到临时登陆凭证code
  • 小程序调用微信的获取手机号接口,拿到加密后的手机号数据PhoneData、加密向量iv
  • 小程序调用后端服务,将PhoneData、code、iv传入后端
  • 后端根据code获取到解密需要的参数:sessionKey
  • 后端通过AES解密PhoneData,拿到用户手机号
  • 后端根据拿到的手机号,做免密登陆(一键登陆),返回给前端用户信息、登陆的token信息等

本文主要讲的是用Go语言实现和微信端交互、解密拿到手机号的部分

微信登陆流程


这是官网给出的流程图,实际的实现,略有差异

第一步:调用 wx.login() 获取 临时登录凭证code ,并回传到开发者服务器。

第二步:获取手机号:getPhoneNumber()

以上两步在小程序内完成

Go后端实现

code2session

这一步主要是为了拿到解密数据需要的sessionKey:code2Session()

首先定义好需要的几个struct

package dto

//封装前端传入的数据
type ConfirmLoginDto struct {
    
    
	PhoneData string `json:"phone_data"`
	Iv        string `json:"iv"`
	WxCode    string `json:"wx_code"`
}

//封装code2session接口返回数据
type WxSessionKeyDto struct {
    
    
	OpenId     string `json:"openid"`
	SessionKey string `json:"session_key"`
	UnionId    string `json:"unionid"`
	ErrCode    int    `json:"errcode"`
	ErrMsg     string `json:"errmsg"`
}

//封装手机号信息数据
type WxPhoneDto struct {
    
    
	PhoneNumber     string `json:"phoneNumber"`
	PurePhoneNumber string `json:"purePhoneNumber"`
	CountryCode     string `json:"countryCode"`
}

获取sessionKey

var(
  	appId     = ""
		appSecret = ""
		url = "https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code"
)

func code2session(code string) (dto.WxSessionKeyDto, error) {
    
    
	var sessionKeyDto dto.WxSessionKeyDto
	httpState, bytes := util.Get(fmt.Sprintf(url, appId, appSecret, code))
	if httpState != 200 {
    
    
		klog.Errorf("获取sessionKey失败,HTTP CODE:%d", httpState)
		return sessionKeyDto, errors.New("获取sessionKey失败")
	}
	e := json.Unmarshal(bytes, &sessionKeyDto)
	if e != nil {
    
    
		klog.Error("json解析失败", e)
		return sessionKeyDto, errors.New("json解析失败")
	}
	return sessionKeyDto, nil
}

Http工具类

// Get HTTP GET request
func Get(url string) (int, []byte) {
    
    
	client := &http.Client{
    
    Timeout: 5 * time.Second}
	resp, err := client.Get(url)
	if err != nil {
    
    
		klog.Errorf("error sending GET request, url: %s, %q", url, err)
		return http.StatusInternalServerError, nil
	}
	defer resp.Body.Close()
	var buffer [512]byte
	result := bytes.NewBuffer(nil)
	for {
    
    
		n, err := resp.Body.Read(buffer[0:])
		result.Write(buffer[0:n])
		if err != nil {
    
    
			if err == io.EOF {
    
    
				break
			}
			klog.Errorf("error decoding response from GET request, url: %s, %q", url, err)
		}
	}
	return resp.StatusCode, result.Bytes()
}

AES解密

func decryptPhoneData(phoneData, sessionKey, iv string) (string, error) {
    
    
	decrypt, err := util.AesDecrypt(phoneData, sessionKey, iv)
	if err != nil {
    
    
		klog.Error("解密数据失败", err)
		return "", err
	}
	var phoneDto = dto.WxPhoneDto{
    
    }
	err = json.Unmarshal(decrypt, &phoneDto)
	if err != nil {
    
    
		klog.Error("解析手机号信息失败", err)
		return "", err
	}
	var phone = phoneDto.PurePhoneNumber
	return phone, nil
}

解密工具类(AES/CBC/PKCS7Padding)

package util

import (
	"crypto/aes"
	"crypto/cipher"
	"encoding/base64"
)

func AesDecrypt(encryptedData, sessionKey, iv string) ([]byte, error) {
    
    
	//Base64解码
	keyBytes, err := base64.StdEncoding.DecodeString(sessionKey)
	if err != nil {
    
    
		return nil, err
	}
	ivBytes, err := base64.StdEncoding.DecodeString(iv)
	if err != nil {
    
    
		return nil, err
	}
	cryptData, err := base64.StdEncoding.DecodeString(encryptedData)
	if err != nil {
    
    
		return nil, err
	}
	origData := make([]byte, len(cryptData))
	//AES
	block, err := aes.NewCipher(keyBytes)
	if err != nil {
    
    
		return nil, err
	}
	//CBC
	mode := cipher.NewCBCDecrypter(block, ivBytes)
	//解密
	mode.CryptBlocks(origData, cryptData)
	//去除填充位
	origData = PKCS7UnPadding(origData)
	return origData, nil
}

func PKCS7UnPadding(plantText []byte) []byte {
    
    
	length := len(plantText)
	if length > 0 {
    
    
		unPadding := int(plantText[length-1])
		return plantText[:(length - unPadding)]
	}
	return plantText
}

通过上面两步就可以拿到用户的手机号,拿到手机号之后,就可以通过手机号在自己的系统内做查询用户信息、免密登陆等操作了

接口部分

var (
	//code状态
	initial   = "1" //初始
	scaned    = "2" //已扫描
	confirmed = "3" //已确认
	expired   = "4" //已过期
)
func Confirm(c *gin.Context) {
    
    
	var confirmDto dto.ConfirmLoginDto
	err := c.BindJSON(&confirmDto)
	if err != nil {
    
    
		klog.Errorf("no valid dto present in request body")
		helper.ReqMissParams(c, confirmDto)
		return
	}
	//检查code状态
	cache, err := db.GetStrInCache(confirmDto.WxCode)
	if err != nil && err != redis.Nil {
    
    
		helper.ReqInternalError(c, "获取code缓存失败", err)
		return
	}
	if len(cache) == 0 {
    
    
		helper.ReqSuccess(c, expired)
		return
	}
	//只有状态为已扫描才能执行下一步确认操作
	if cache != scaned {
    
    
		helper.ReqFailedProcess(c, types.ErrCodeStatus)
	}
	//1.查询解密需要的sessionKey
	keyDto, err := code2session(confirmDto.WxCode)
	if err != nil {
    
    
		helper.ReqInternalError(c, "获取微信数据失败", err)
		return
	}
	//2.解密,拿到手机号
	phone, err := decryptPhoneData(confirmDto.PhoneData, keyDto.SessionKey, confirmDto.Iv)
	if err != nil {
    
    
		helper.ReqInternalError(c, "解析手机号失败", err)
		return
	}
	fmt.Println(phone)
	
	//做自己的业务操作:校验用户是否注册/登陆/获取用户信息等
	
}

以上是描述了大致的实现步骤,具体的话,还有很多细节。

Guess you like

Origin blog.csdn.net/sinat_14840559/article/details/121636520