需求
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"`
}
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工具类
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) {
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))
block, err := aes.NewCipher(keyBytes)
if err != nil {
return nil, err
}
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 (
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
}
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)
}
keyDto, err := code2session(confirmDto.WxCode)
if err != nil {
helper.ReqInternalError(c, "获取微信数据失败", err)
return
}
phone, err := decryptPhoneData(confirmDto.PhoneData, keyDto.SessionKey, confirmDto.Iv)
if err != nil {
helper.ReqInternalError(c, "解析手机号失败", err)
return
}
fmt.Println(phone)
}
以上是描述了大致的实现步骤,具体的话,还有很多细节。
|