微信小程序(抖音小程序):手機(jī)號碼解析失敗解決方案

在小程序開發(fā)中米奸,可能需要用戶授權(quán)獲取用戶信息,而用戶信息涉及到手機(jī)號等敏感數(shù)據(jù)悔叽,一般的小程序開發(fā)平臺,會將數(shù)據(jù)進(jìn)行加密爵嗅,然后通過對稱加密算法進(jìn)行加密解密娇澎。在獲取手機(jī)號的過程中由于流程的理解錯誤可能會出現(xiàn)解密手機(jī)號失敗的問題。本文介紹一種比較適用的解決辦法睹晒,希望給遇到坑的同學(xué)一個參考趟庄。

1 問題描述

本文以抖音小程序(微信小程序獲取流程和接口一模一樣)為例,最近博主在做一個抖音小程序的小項(xiàng)目伪很,前端在獲取用戶手機(jī)號的時候戚啥,需要調(diào)用tt.login接口進(jìn)行登錄,登錄后返回一個code锉试,這個code有3分鐘的失效時間猫十,根據(jù)這個code可以獲取到sessionKey,這個sessionKey類似于對稱加密的密鑰呆盖,會對用戶信息進(jìn)行加密拖云。在獲取用戶信息的時候,前端
需要將 <button> 組件 open-type 的值設(shè)置為 getPhoneNumber应又。用戶點(diǎn)擊后會彈出一個授權(quán)彈窗讓用戶確認(rèn)(若該用戶賬戶未綁定手機(jī)號碼會執(zhí)行一次綁定手機(jī)號碼的流程宙项;授權(quán)彈窗每次使用都會彈出)。 用戶同意后株扛,開發(fā)者可以通過 bindgetphonenumber 事件回調(diào)獲取到一個加密數(shù)據(jù)尤筐,開發(fā)者可以把該數(shù)據(jù)傳回到自己的服務(wù)端進(jìn)行解密獲取手機(jī)號。

獲取到的加密數(shù)據(jù)需要使用sessionKey進(jìn)行解密洞就,因此在獲取用戶信息前盆繁,需要登錄一次,獲取到code奖磁,然后根據(jù)code獲取到sessionKey改基,再根據(jù)sessionKey進(jìn)行加密數(shù)據(jù)的解密,解析出手機(jī)號咖为。

根據(jù)博主猜測秕狰,抖音在登錄后會生成一個code,和一個對應(yīng)的sessionKey躁染,在會話期間(session未過期)的時候獲取用戶信息鸣哀,會將用戶信息使用sessionKey進(jìn)行數(shù)據(jù)的加密,進(jìn)行數(shù)據(jù)的解密也需要使用到sessionKey吞彤。code和sessionKey是對應(yīng)的我衬,但是它們的失效期是不一樣的叹放,code的失效期是3分鐘,sessionKey的失效時間是不定的挠羔,只要用戶活躍在頁面上都不會失效井仰。在獲取到code的3分鐘內(nèi)調(diào)用code-2-session接口,會獲取到sessionKey破加,如果3分鐘后根據(jù)code獲取sessionKey將會獲取失敗俱恶,因此解密也會失敗。

1.1 初始實(shí)現(xiàn)

因?yàn)闊o法判斷用戶什么時候開始獲取用戶信息范舀,所以用戶一進(jìn)入頁面合是,前端就會調(diào)用tt.login接口進(jìn)行登錄,然后放到localstorage緩存中锭环,在用戶點(diǎn)擊按鈕時聪全,彈出授權(quán)框用戶確認(rèn)后獲取到用戶信息的加密數(shù)據(jù),然后前端將緩存的code和加密數(shù)據(jù)一并傳給后端辅辩。后端用code先去調(diào)用code-2-session接口獲取到sessionKey难礼,然后以sessionKey為密鑰進(jìn)行AES解密,獲取到手機(jī)號返回給前臺汽久。整個流程看起來沒什么問題鹤竭,但是一旦用戶在頁面停留時間超過3分鐘,然后再去獲取用戶信息會失敗景醇,主要是因?yàn)閏ode已經(jīng)失效臀稚,獲取sessionKey會失敗。

image.png

2 解決辦法

2.1 緩存sessionKey

目前的問題就是過了code的有效期后三痰,根據(jù)code獲取sessionKey失敗吧寺。那么在前端login獲取到code后,先緩存到本地散劫,然后立即調(diào)用后臺接口去獲取sessionKey然后緩存到redis里面稚机,key為code,value為sessionKey获搏。失效時間根據(jù)自己的業(yè)務(wù)設(shè)置(小程序頁面用戶不會停留太久赖条,因此緩存失效時間設(shè)置為30分鐘),用戶退出小程序后常熙,會重新login纬乍,然后也會存一份新的code和sessionKey的對應(yīng)值。

用戶在授權(quán)到用戶信息后裸卫,前端直接將緩存的code和加密后的用戶信息上傳到服務(wù)到進(jìn)行解密仿贬。服務(wù)端根據(jù)code從緩存中先獲取到sessionKey,然后再用sessionKey進(jìn)行解密墓贿,解析出手機(jī)號進(jìn)行返回茧泪。

image.png

2.2 存在問題

以上解決辦法每次基本都可以獲取手機(jī)號成功蜓氨,但是也會存在一些問題

  • 會存在很多冗余數(shù)據(jù):因?yàn)榫彺媸歉鶕?jù)code進(jìn)行緩存的,無法根據(jù)用戶唯一id進(jìn)行緩存队伟,如果用戶多次進(jìn)行登錄穴吹,將會存儲多份,因此需要根據(jù)自己的業(yè)務(wù)時間進(jìn)行設(shè)置緩存失效時間
  • 實(shí)現(xiàn)更加復(fù)雜:因?yàn)楹蠖诉€涉及到redis服務(wù) 以及加密解密的過程

3 附上源碼

3.1 用戶信息controller

UserInfoController主要提供兩個接口缰泡,一個是解密手機(jī)號和code2seesion操作

@Api("用戶信息")
@Validated
@RestController
public class UserInfoController {

    @Resource
    TiktokUserInfoSPI tiktokUserInfoSPI;

    @ApiOperation("解密手機(jī)號")
    @PostMapping("/api/userinfo/decrypt/phone")
    public Result<PhoneResult> decryptPhone(@Validated @RequestBody TiktokEncryptedParam param) {
        return Result.success(tiktokUserInfoSPI.decryptUserPhone(param));
    }

    @ApiOperation("code2seesion")
    @GetMapping("/api/userinfo/code2session")
    public Result code2Session(@RequestParam("code") @NotEmpty(message = "code不能為空") String code) {
        tiktokUserInfoSPI.code2Session(code);
        return Result.success(null);
    }
}

TiktokEncryptedParam 主要是前端傳過來的code和加密后的數(shù)據(jù)

/**
 * @ClassName : TiktokEncryptedParam
 * @Description : 抖音小程序用戶加密參數(shù)
 */
@Data
@ApiModel("抖音小程序加密參數(shù)")
public class TiktokEncryptedParam {

    @NotEmpty(message = "code不能為空")
    @ApiModelProperty(value="login 接口返回的登錄憑證",name="code")
    private String code;

    @ApiModelProperty(value="login 接口返回的匿名登錄憑證",name="anonymousCode")
    private String anonymousCode;

    @NotEmpty(message = "加密數(shù)據(jù)不能為空")
    @ApiModelProperty(value="加密數(shù)據(jù)",name="encryptedData")
    private String encryptedData;

    @NotEmpty(message = "加密初始向量不能為空")
    @ApiModelProperty(value="加密初始向量",name="iv")
    private String iv;
}
3.2 抖音接口SPI

TiktokUserInfoSPI 主要是對接口的封裝

public interface TiktokUserInfoSPI {

    /**
     * 解密敏感數(shù)據(jù)獲取手機(jī)號
     * @param param
     * @return
     */
    PhoneResult decryptUserPhone(TiktokEncryptedParam param);

    /**
     * 通過login接口獲取到登錄憑證后刀荒,開發(fā)者可以通過服務(wù)器發(fā)送請求的方式獲取 session_key 和 openId。
     * @param code
     * @return
     */
    Code2SessionResult code2Session(String code);
}

TiktokUserInfoSPIAdapter 實(shí)現(xiàn)接口


import cn.hutool.core.util.ObjectUtil;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;

@Slf4j
@Service
public class TiktokUserInfoSPIAdapter implements TiktokUserInfoSPI {

    @Value("${tiktok.miniprogram.appid}")
    private String appId;

    @Value("${tiktok.miniprogram.secret}")
    private String secret;

    @Qualifier("getRedisson")
    @Autowired
    RedissonClient redissonClient;

    @Override
    public PhoneResult decryptUserPhone(TiktokEncryptedParam param) {
        if(ObjectUtil.isEmpty(param.getCode())) {
            throw new BusinessException(ToastConstant.ERR_JSCODE);
        }
        PhoneResult phoneResult = new PhoneResult();
        // code2Session
        Code2SessionResult result = getSessionResult(param.getCode());
        if(result.getErrCode() != 0) {
            phoneResult.setErrCode(result.getErrCode());
            phoneResult.setErrMsg(result.getErrMsg());
            return phoneResult;
        }
        log.info("開始進(jìn)行數(shù)據(jù)解密------- param = [{}]" , JSONUtil.toJsonStr(param));
        String jsonString = DecryptUtil.decrypt(param.getEncryptedData(), result.getSessionKey(), param.getIv());
        log.info("解密后的數(shù)據(jù)為-------  jsonString = [{}]" , jsonString);
        PhoneNumberResult phoneNumberResult = JSONUtil.toBean(jsonString, PhoneNumberResult.class);
        phoneResult.setErrCode(0);
        phoneResult.setPhone(phoneNumberResult.getPurePhoneNumber());
        return phoneResult;
    }

    private Code2SessionResult getSessionResult(String code) {
        String cacheKey = String.format(RedisConstant.CACHE_KEY.TIKTOK_SESSION_KEY, code);
        RBucket<Code2SessionResult> bucket = this.redissonClient.getBucket(cacheKey);
        Code2SessionResult result = bucket.get();
        if(ObjectUtil.isNull(result) || ObjectUtil.isEmpty(result.getSessionKey())) {
            result = new Code2SessionResult();
            result.setErrCode(ErrCodeEnum.FAIL.getCode());
            result.setErrMsg(ToastConstant.ERROR_GET_SESSION_KEY);
        }
        return result;
    }

    @Override
    public Code2SessionResult code2Session(String code) {
        // 構(gòu)造參數(shù)
        Map<String, Object> paramMap = new HashMap<>();
        paramMap.put("appid", appId);
        paramMap.put("secret", secret);
        paramMap.put("code", code);
        // 發(fā)送請求
        String jsonResult = HttpUtil.get(ApiConstant.BYTEDANCE_TIKTOK.JSCODE_2SESSION, paramMap);
        if(ObjectUtil.isNull(jsonResult)) {
            log.error("獲取sessionKey失敗, jsonResult 返回為空");
            //throw new BusinessException(ToastConstant.ERROR_GET_SESSION_KEY);
        }
        // 解析結(jié)果
        JSONObject jsonObject = JSONUtil.parseObj(jsonResult);
        int error = jsonObject.getInt("error");
        Code2SessionResult result = new Code2SessionResult();
        if(error == ErrCodeEnum.SUCCESS.getCode()) {
            result.setOpenId(jsonObject.getStr("openid"));
            result.setSessionKey(jsonObject.getStr("session_key"));
            result.setAnonymousOpenId(jsonObject.getStr("anonymous_openid"));
            result.setUnionId(jsonObject.getStr("unionid"));
            result.setErrCode(ErrCodeEnum.SUCCESS.getCode());
            //return result;
        } else {
            int errCode = jsonObject.getInt("errcode");
            String errMsg = jsonObject.getStr("errmsg");
            // code錯誤棘钞,可能是登錄失效
            if(errCode == Code2SessionEnum.ERROR_40018.getCode() ||
                    errCode == Code2SessionEnum.ERROR_40019.getCode()) {
                result.setErrCode(errCode);
                result.setErrMsg(errMsg);
                //return result;
            }
            log.error("獲取sessionKey失敗, errCode = [{}], errMsg = [{}]", errCode, errMsg);
            //throw new BusinessException(ToastConstant.ERROR_GET_SESSION_KEY);
        }
        String cacheKey = String.format(RedisConstant.CACHE_KEY.TIKTOK_SESSION_KEY, code);
        RBucket<Code2SessionResult> bucket = this.redissonClient.getBucket(cacheKey);
        bucket.set(result, 30, TimeUnit.MINUTES);
        return result;
    }

}
3.3 加密解密

使用AES對稱加密


import cn.hutool.crypto.symmetric.AES;
import com.tiktokminiprogram.exception.BusinessException;
import lombok.extern.slf4j.Slf4j;

import java.util.Base64;

/**
 * @ClassName : DecryptUtil
 * @Description : 解密工具類
 */
@Slf4j
public class DecryptUtil {

    /**
     * 解密敏感數(shù)據(jù)
     * @param encryptedData
     * @param sessionKey
     * @param iv
     * @return
     */
    public static String decrypt(String encryptedData, String sessionKey, String iv) {
        try {
            Base64.Decoder decoder = Base64.getDecoder();
            byte[] sessionKeyBytes = decoder.decode(sessionKey);
            byte[] ivBytes = decoder.decode(iv);
            byte[] encryptedBytes = decoder.decode(encryptedData);

            AES aes = new AES("CBC", "PKCS7Padding", sessionKeyBytes, ivBytes);
            String res = aes.decryptStr(encryptedBytes);
            log.info("res = [{}]", res);
            return res;
        } catch (Exception e) {
            log.error("解密出現(xiàn)錯誤", e);
            throw new BusinessException("解密出現(xiàn)錯誤");
        }
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市干毅,隨后出現(xiàn)的幾起案子宜猜,更是在濱河造成了極大的恐慌,老刑警劉巖硝逢,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件姨拥,死亡現(xiàn)場離奇詭異,居然都是意外死亡渠鸽,警方通過查閱死者的電腦和手機(jī)叫乌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來徽缚,“玉大人憨奸,你說我怎么就攤上這事≡涫裕” “怎么了排宰?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長那婉。 經(jīng)常有香客問我板甘,道長,這世上最難降的妖魔是什么详炬? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任盐类,我火速辦了婚禮,結(jié)果婚禮上呛谜,老公的妹妹穿的比我還像新娘在跳。我一直安慰自己,他們只是感情好呻率,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布硬毕。 她就那樣靜靜地躺著,像睡著了一般礼仗。 火紅的嫁衣襯著肌膚如雪吐咳。 梳的紋絲不亂的頭發(fā)上逻悠,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天,我揣著相機(jī)與錄音韭脊,去河邊找鬼童谒。 笑死,一個胖子當(dāng)著我的面吹牛沪羔,可吹牛的內(nèi)容都是我干的饥伊。 我是一名探鬼主播,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼蔫饰,長吁一口氣:“原來是場噩夢啊……” “哼琅豆!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起篓吁,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤茫因,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后杖剪,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體冻押,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年盛嘿,在試婚紗的時候發(fā)現(xiàn)自己被綠了洛巢。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡次兆,死狀恐怖稿茉,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情类垦,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站蘸嘶,受9級特大地震影響训唱,放射性物質(zhì)發(fā)生泄漏况增。R本人自食惡果不足惜歧强,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望茅特。 院中可真熱鬧棋枕,春花似錦熬荆、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至蓝牲,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間佛玄,已是汗流浹背梦抢。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工哼蛆, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留人芽,地道東北人萤厅。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像名挥,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子救湖,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內(nèi)容