小程序快捷登录

小程序授权登录

目录

一、获取unionId

二、获取session_key

三、获取手机号

四、小程序通过手机号快速授权登录

五、开发中的坑


 

一、获取unionId

 

  1. 通过 wx.login 接口获得临时登录凭证 code 后传到开发者服务器调用此接口完成登录流程
     

    请求地址

     
    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
    grant_typestring 授权类型,此处只需填写 authorization_code

    返回值

    Object

    返回的 JSON 数据包

    属性类型说明
    openidstring用户唯一标识
    session_keystring会话密钥
    unionidstring用户在开放平台的唯一标识符,在满足 UnionID 下发条件的情况下会返回,详见 UnionID 机制说明
    errcodenumber错误码
    errmsgstring错误信息

2.用户未关注公众号等相关主体账号时,通过code获取不到unionId时,则通过session_key、encryptedData、iv获取unionId

 

encryptedDatastring包括敏感数据在内的完整用户信息的加密数据
ivstring加密算法的初始向量

 

接口如果涉及敏感数据(如wx.getUserInfo当中的 openId 和 unionId),接口的明文内容将不包含这些敏感数据。开发者如需要获取敏感数据,需要对接口返回的加密数据(encryptedData) 进行对称解密。 解密算法如下:

  1. 对称解密使用的算法为 AES-128-CBC,数据采用PKCS#7填充。
  2. 对称解密的目标密文为 Base64_Decode(encryptedData)。
  3. 对称解密秘钥 aeskey = Base64_Decode(session_key), aeskey 是16字节。
  4. 对称解密算法初始向量 为Base64_Decode(iv),其中iv由数据接口返回。

如接口 wx.getUserInfo 敏感数据当中的 watermark:

{
    "openId": "OPENID",
    "nickName": "NICKNAME",
    "gender": GENDER,
    "city": "CITY",
    "province": "PROVINCE",
    "country": "COUNTRY",
    "avatarUrl": "AVATARURL",
    "unionId": "UNIONID",
    "watermark":
    {
        "appid":"APPID",
        "timestamp":TIMESTAMP
    }
}

a.首先通过code获取到session_key

b.通过session_key解密,从解密后的接送中提取unionId

小程序前端传递对象

@Data
@ApiModel(value = "MiniProgramRequest",description = "小程序加密数据")
public class MiniProgramRequest {

    @ApiModelProperty(value = "前端code")
    private String code;

    @ApiModelProperty(value = "加密数据")
    private String encryptedData;

    @ApiModelProperty(value = "加密向量")
    private String iv;

    @ApiModelProperty(value = "openId")
    private String openId;

}

 

    /**
     * 获取unionId
     * @param code
     * @return unionId
     */
    public static String getUnionId(String code,String encryptedData,String iv){
        String  result = WebUtils.get(ACCESS_TOKEN_REQUEST_URL +"?appid=" + WX_LITE_APP_ID + "&secret=" + WX_LITE_APP_SECRET + "&js_code="+code+"&grant_type=authorization_code"  ,null);
        JsonObject jsonObject = GsonUtil.fromObject(result);
        String unionId = "";
        //用户授权直接获取unionId
        if(result.contains("unionid")){
            unionId = jsonObject.get("unionid").getAsString();
        }
        //用户未关注公众号等主体账号时,通过session_key解密获取unionId
        if (result.contains("session_key")){
            String sessionKey = jsonObject.get("session_key").getAsString();
            String s = AESDescryptUtil.decrypt(encryptedData, sessionKey, iv);
            JsonObject userInfoJson = GsonUtil.fromObject(s);
            unionId = userInfoJson.get("unionId").getAsString();
        }
        return unionId;
    }

解密代码


import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.Security;
import java.util.Arrays;

/**
 * @Author: sean.xu
 * @Date: 2019/12/4
 */
@Slf4j
public class AESDescryptUtil {

    // 算法名称
    private static final String KEY_ALGORITHM = "AES";

    /**
     * 加解密算法/模式/填充方式
     */
    private static final String algorithmStr = "AES/CBC/PKCS7Padding";

    private static Key key;
    private static Cipher cipher;

    public static String encrypt(byte[] originalContent, byte[] encryptKey, byte[] ivByte) {
        try {
            encryptKey = Base64.decodeBase64(encryptKey);
            ivByte = Base64.decodeBase64(ivByte);

            init(encryptKey);
            cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(ivByte));
            byte[] encrypted = cipher.doFinal(originalContent);
            return new String(Base64.encodeBase64(encrypted), StandardCharsets.UTF_8);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private static void init(byte[] keyBytes) {

        // 如果密钥不足16位,那么就补足. 这个if 中的内容很重要
        int base = 16;
        if (keyBytes.length % base != 0) {
            int groups = keyBytes.length / base + (keyBytes.length % base != 0 ? 1 : 0);
            byte[] temp = new byte[groups * base];
            Arrays.fill(temp, (byte) 0);
            System.arraycopy(keyBytes, 0, temp, 0, keyBytes.length);
            keyBytes = temp;
        }
        // 初始化
        Security.addProvider(new BouncyCastleProvider());
        // 转化成JAVA的密钥格式
        key = new SecretKeySpec(keyBytes, KEY_ALGORITHM);
        try {
            // 初始化cipher
            cipher = Cipher.getInstance(algorithmStr);
        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
            log.error("AES加密算法出错",e);
        }
    }

    /**
     * 解密方法
     * @param encryptedDataStr
     * @param keyBytesStr
     * @param ivStr
     * @return
     */
    public static String decrypt(String encryptedDataStr, String keyBytesStr, String ivStr) {
        byte[] encryptedText = null;
        byte[] encryptedData;
        byte[] sessionkey;
        byte[] iv;

        try {
            sessionkey = Base64.decodeBase64(keyBytesStr);
            encryptedData = Base64.decodeBase64(encryptedDataStr);
            iv = Base64.decodeBase64(ivStr);

            if (StringUtils.isNotEmpty(sessionkey)){
                init(sessionkey);
                cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
                encryptedText = cipher.doFinal(encryptedData);
            }
        } catch (Exception e) {
            log.error("AES解密算法出错",e);
        }
        assert encryptedText != null;
        return new String(encryptedText,StandardCharsets.UTF_8);
    }

}

 

二、获取session_key

  1. 小程序点击个人中心,首先会调getSessionKey,后端通过传递的code获取到session_key保存到redis,用于后续获取手机号
  2. session_key的过期时间通过小程序的checkSession检查,不需要后端做任何操作,获取到的session_key也不需要返还给小程序,只需自己保存即可
    public static String getSessionKey(MiniProgramRequest miniProgram){
        String  result = WebUtils.get(ACCESS_TOKEN_REQUEST_URL +"?appid=" + WX_LITE_APP_ID + "&secret=" + WX_LITE_APP_SECRET + "&js_code="+miniProgram.getCode()+"&grant_type=authorization_code"  ,null);
        JsonObject jsonObject = GsonUtil.fromObject(result);
        String sessionKey =null;
        if(result.contains("session_key")) {
            sessionKey = jsonObject.get("session_key").getAsString();
            RedisUtils.set(SESSION_KEY + miniProgram.getOpenId(),sessionKey);
        }
        return sessionKey;
    }

 

三、获取手机号

文档:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/getPhoneNumber.html

  1. 小程序调getPhoneNumber,传给后端encryptedData,iv。后端通过之前redis保存的session_key解密数据
  2. 解密坑很多(前端必须将传递的数据通过encode处理,防止数据传递丢失,同时encryptedData,iv必须保证16个字节长度)
  3. 解密成功返给前端手机号,前端拿到手机号后通过手机号授权快速登录

 

获取得到的开放数据为以下 json 结构:

{
    "phoneNumber": "13580006666",
    "purePhoneNumber": "13580006666",
    "countryCode": "86",
    "watermark":
    {
        "appid":"APPID",
        "timestamp": TIMESTAMP
    }
}

 

参数类型说明
phoneNumberString用户绑定的手机号(国外手机号会有区号)
purePhoneNumberString没有区号的手机号
countryCodeString区号
    /**
     *  获取手机号
     *  如果前端传code,则通过code请求小程序接口获取新的session_key
     *  如果前端没有传code,则获取之前redis保存的session_key
     * @return
     */
    public static String  getPhoneNumber(MiniProgramRequest miniProgram){
        String sessionKey = (String)RedisUtils.get(SESSION_KEY + miniProgram.getOpenId());
        String phoneNumber = null;
        if (StringUtils.isNotEmpty(sessionKey)){
            String s = AESDescryptUtil.decrypt(miniProgram.getEncryptedData(), sessionKey, miniProgram.getIv());
            JsonObject userInfoJson = GsonUtil.fromObject(s);
            phoneNumber = userInfoJson.get("phoneNumber").getAsString();

        }
        return phoneNumber;
    }

 

四、小程序通过手机号快速授权登录

 

  1. 通过手机号查询会员(手机号为之前解密传给前端的手机号)
  2. 会员存在,直接将会员数据返回给小程序
  3. 会员不存在,直接注册新会员,并将数据返回给小程序(小程序快捷登录的不绑定第三方)

 

五、开发中的坑

  1. 前端传过来的code,通过后台请求微信接口的时候,code只能用一次,重复使用小程序报错msg:code been used, hints
  2. 获取手机号的时候,需要用到session_key,在刚开始通过code获取到session_key的时候,可以放到redis里,等下次用的时候直接在redis拿,切忌不可将session_key返回前端然后在获取手机号的时候通过前端传递
  3. 保存session_key的到redis时候,一定要记得redis的key要唯一,最开始我就是直接保存的,导致多个用户同时登录时候,只有一个能登录成功,其实是因为他们共用了同一个session_key。一定要加以区分,可以把openId传过来放入key中作为唯一约束
  4. 也许前端生成的加密数据和加密向量是正常的,直接复制给我们调微信接口可以获取到数据,但是在传递的时候可能会丢失字符,比如%等字符会解析成其他字符,可通过base64处理
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值