1. 程式人生 > >微信小程式全棧(一).通過授權登入解密獲取使用者的openid和unionid

微信小程式全棧(一).通過授權登入解密獲取使用者的openid和unionid

    最近做一個微信小程式,有個功能需要甲觸發A事件然後B收到甲的訊息。毫無疑問就是要獲取openid存下來與使用者綁定了。然後可能有公眾號對接進來,於是還是要求一個unionid。這就不可避免要涉及一個獲取流程。

   本來我以為是小程式端簡簡單單獲取一下騰訊的介面直接就返回回資料了,後來去官方文件看了一下是前端把密文,偏移量,code發給後臺,後臺通過騰訊介面傳送自己的AppId,AppSecret和認證型別獲取一個json物件,再用這個json物件解析出session_key,即會話金鑰,從而通過和密文,偏移量解密得出使用者的資訊放到hashmap裡面再進行自己的業務。

wxml檔案

<!--pages/authorize/index.wxml-->
<view class="container">
<form bindsubmit="bindSave">
<view style='width:100%;padding-left:30rpx;font-size: 28rpx;margin-top:30rpx;'>1.同意當前小程式獲得我的微信頭像;</view>
<view style='width:100%;padding-left:30rpx;font-size: 28rpx;margin-top:30rpx;'>2.同意當前小程式獲取我的微信暱稱等其他資訊;</view>
<button open-type="getUserInfo" bindgetuserinfo="login" class="weui-button">授權並登入</button>
</form>
<!-- <view class="logged" >
<image class="logged-icon" src="../../images/"/>
<view class="logged-text">近期你已經授權登陸過{{title}}</view>
<view class="logged-text">自動登入中</view>
</view> -->
</view>

wxss檔案

/* pages/authorize/index.wxss */
page{
  height: 100%;
}

.container{
  background-color: #f5f5f9;
  justify-content: initial;
}

.form-box{
  width: 100%;
  background-color: #fff;
  margin-top: 20rpx;
}
.row-wrap{
    width: 720rpx;
    height: 88rpx;
    line-height: 88rpx;
    margin-left: 30rpx;
    border-bottom: 1rpx solid #eee;
    display: flex;
    font-size: 28rpx;
    /*justify-content: space-between;*/
}
.row-wrap .label{
    width: 160rpx;
    color: #000
}
.row-wrap .label-right{
    flex: 1;
    height: 88rpx;
    line-height: 88rpx;
}
.row-wrap .label-right input{
    height: 100%;
    font-size: 28rpx;
    padding-right: 30rpx;
}
.row-wrap .right-box{
    margin-right: 30rpx; 
}
.arrow-right{
    width: 15rpx;
    height: 24rpx;
}
.save-btn,
.cancel-btn{
    width: 690rpx;
    height: 80rpx;
    line-height: 80rpx;
    text-align: center;
    margin-top:30rpx; 
    border-radius: 6rpx;
    box-sizing: border-box;
}
.save-btn{
    background-color: #e64340;
    color:#fff;
}
button[type="default"]{
    background-color: #ffffff;
    color:#000;
}
.addr-details{
    height: auto;
    padding: 30rpx 0;
    margin-left:30rpx;
    border-bottom: 1rpx solid #eee;
    display: flex;
    font-size: 28rpx;
}
.addr-details .label{
    margin:auto 0 auto 0;
    width: 160rpx;
    color: #000
}
.addr-details textarea{
    box-sizing: border-box;
    width: 480rpx;
    overflow: scroll;
}
picker {
    min-width: 20rpx;
    height: 100%;
    margin-right: 20rpx;
}
.hui{
    color: #777;
}
.logged {
  margin-top: 100px;
}

.logged .logged-icon {
  display: block;
  width: 64px;
  height: 64px;
  margin: 20px auto;
}

.logged .logged-text {
  font-size: 14px;
  color: #000;
  text-align: center;
  margin: 10px 0;
}
// pages/authorize/index.js
var app=getApp();
var API_URL = app.globalData.baseUrl +'/userMobile'+'/login';
Page({

  /**
   * 頁面的初始資料
   */
  data: {

  },

  /**
   * 生命週期函式--監聽頁面載入
   */
  onLoad: function (options) {

  },

  /**
   * 生命週期函式--監聽頁面初次渲染完成
   */
  onReady: function () {

  },

  /**
   * 生命週期函式--監聽頁面顯示
   */
  onShow: function () {

  },

  /**
   * 生命週期函式--監聽頁面隱藏
   */
  onHide: function () {

  },

  /**
   * 生命週期函式--監聽頁面解除安裝
   */
  onUnload: function () {

  },

  /**
   * 頁面相關事件處理函式--監聽使用者下拉動作
   */
  onPullDownRefresh: function () {

  },

  /**
   * 頁面上拉觸底事件的處理函式
   */
  onReachBottom: function () {

  },

  /**
   * 使用者點選右上角分享
   */
  onShareAppMessage: function () {

  },
  login:function(e){
    wx.login({
      success: function(r){
        var code=r.code;//登陸憑證
        console.log(code)
        if(code){
          wx.getUserInfo({
            success: function(res){
              console.log({encryptedData: res.encryptedData,iv:res.iv,code:code})
              wx.request({
                url: API_URL,
                method: 'POST',
                header: {
                  'content-type': 'application/json'
                },
                data:{
                 "encryptedData": res.encryptedData,
                 "iv": res.iv,
                 "code":code
                },
                success: function(data){
                  //解密成功後返回自己伺服器返回的結果
                  if(data.data.status==1){
                     var userInfo_=data.data.userInfo;
                     app.globalData.userInfo=userInfo_;
                     console.log(userInfo_)
                    wx.redirectTo({
                      url: '../../pages/area/area',
                    })
                  }else{
                    console.log('解密失敗')
                  }
                },
                fail: function(){
                  console.log('系統錯誤')
                }
              })
            },
            fail:function(){
              console.log('獲取使用者資訊失敗')
            }
          })
        }
      }
    })
  }
})

然後是後臺程式碼:

 @ApiOperation("小程式使用者登陸")
   @PostMapping("/login")
    public R decodeUserInfo(@RequestBody MobileUser mobileUser){
       if(mobileUser.getCode()==null||mobileUser.getCode().length()==0){
           return R.error("code不能為空").put("status",0);
       }
       String wxspAppid="*******************";//你的appid
       String wxspSecret="******************";//你的appsecret
       String grant_type="authorization_code";//一般都是這個認證型別

       String params="appid="+wxspAppid+"&secret="+wxspSecret+"&js_code="+mobileUser.getCode()+"&grant_type="+grant_type;
       //傳送請求
       String sr= HttpRequest.sendGet("https://api.weixin.qq.com/sns/jscode2session", params);
       JSONObject json= JSON.parseObject(sr);

       String session_key=json.get("session_key").toString();
       String openid=(String) json.get("openid");
       try{
       String result= AesCbcUtil.decrypt(mobileUser.getEncryptedData(),session_key,mobileUser.getIv(),"UTF-8");
       if(null!=result&&result.length()>0){
           JSONObject userInfoJSON=JSON.parseObject(result);
           Map userInfo=new HashMap();
           userInfo.put("openId", userInfoJSON.get("openId"));
           userInfo.put("nickName", userInfoJSON.get("nickName"));
           userInfo.put("gender", userInfoJSON.get("gender"));
           userInfo.put("city", userInfoJSON.get("city"));
           userInfo.put("province", userInfoJSON.get("province"));
           userInfo.put("country", userInfoJSON.get("country"));
           userInfo.put("avatarUrl", userInfoJSON.get("avatarUrl"));
           // 解密unionId & openId;
           if (userInfoJSON.get("unionId")!=null) {
               userInfo.put("unionId", userInfoJSON.get("unionId"));
           }
           return R.ok("成功").put("status",1).put("userInfo",userInfo);
       }else{
           return R.error("失敗").put("status",0);
       }
       }catch(Exception e){
       logger.error(e.toString());
       return R.error();
       }
  //return R.ok();
   }

usermobile的包體

public class MobileUser {
    private String encryptedData;
    private String iv;
    private String code;

    public String getCode() {
        return code;
    }

    public String getEncryptedData() {
        return encryptedData;
    }

    public String getIv() {
        return iv;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public void setEncryptedData(String encryptedData) {
        this.encryptedData = encryptedData;
    }

    public void setIv(String iv) {
        this.iv = iv;
    }

}

後臺接發資料的工具類HttpRequest

public class HttpRequest {
    /**
     * 向指定URL傳送GET方法的請求
     *
     * @param url
     *            傳送請求的URL
     * @param param
     *            請求引數,請求引數應該是 name1=value1&name2=value2 的形式。
     * @return URL 所代表遠端資源的響應結果
     */
    public static String sendGet(String url, String param) {
        String result = "";
        BufferedReader in = null;
        try {
            String urlNameString = url + "?" + param;
            URL realUrl = new URL(urlNameString);
            // 開啟和URL之間的連線
            URLConnection connection = realUrl.openConnection();
            // 設定通用的請求屬性
            connection.setRequestProperty("accept", "*/*");
            connection.setRequestProperty("connection", "Keep-Alive");
            connection.setRequestProperty("user-agent",
                    "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
            // 建立實際的連線
            connection.connect();
            // 獲取所有響應頭欄位
            Map<String, List<String>> map = connection.getHeaderFields();
            // 遍歷所有的響應頭欄位
            for (String key : map.keySet()) {
                System.out.println(key + "--->" + map.get(key));
            }
            // 定義 BufferedReader輸入流來讀取URL的響應
            in = new BufferedReader(new InputStreamReader(
                    connection.getInputStream()));
            String line;
            while ((line = in.readLine()) != null) {
                result += line;
            }
        } catch (Exception e) {
            System.out.println("傳送GET請求出現異常!" + e);
            e.printStackTrace();
        }
        // 使用finally塊來關閉輸入流
        finally {
            try {
                if (in != null) {
                    in.close();
                }
            } catch (Exception e2) {
                e2.printStackTrace();
            }
        }
        return result;
    }

    /**
     * 向指定 URL 傳送POST方法的請求
     *
     * @param url
     *            傳送請求的 URL
     * @param param
     *            請求引數,請求引數應該是 name1=value1&name2=value2 的形式。
     * @return 所代表遠端資源的響應結果
     */
    public static String sendPost(String url, String param) {
        PrintWriter out = null;
        BufferedReader in = null;
        String result = "";
        try {
            URL realUrl = new URL(url);
            // 開啟和URL之間的連線
            URLConnection conn = realUrl.openConnection();
            // 設定通用的請求屬性
            conn.setRequestProperty("accept", "*/*");
            conn.setRequestProperty("connection", "Keep-Alive");
            conn.setRequestProperty("user-agent",
                    "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
            // 傳送POST請求必須設定如下兩行
            conn.setDoOutput(true);
            conn.setDoInput(true);
            // 獲取URLConnection物件對應的輸出流
            out = new PrintWriter(conn.getOutputStream());
            // 傳送請求引數
            out.print(param);
            // flush輸出流的緩衝
            out.flush();
            // 定義BufferedReader輸入流來讀取URL的響應
            in = new BufferedReader(
                    new InputStreamReader(conn.getInputStream()));
            String line;
            while ((line = in.readLine()) != null) {
                result += line;
            }
        } catch (Exception e) {
            System.out.println("傳送 POST 請求出現異常!"+e);
            e.printStackTrace();
        }
        //使用finally塊來關閉輸出流、輸入流
        finally{
            try{
                if(out!=null){
                    out.close();
                }
                if(in!=null){
                    in.close();
                }
            }
            catch(IOException ex){
                ex.printStackTrace();
            }
        }
        return result;
    }

}

解密的方法工具類:

public class AesCbcUtil {


    static {
        //BouncyCastle是一個開源的加解密解決方案,主頁在http://www.bouncycastle.org/
        Security.addProvider(new BouncyCastleProvider());
    }

    /**
     * AES解密
     *
     * @param data      //密文,被加密的資料
     * @param key      //祕鑰
     * @param iv       //偏移量
     * @param encodingFormat //解密後的結果需要進行的編碼
     * @return
     * @throws Exception
     */
    public static String decrypt(String data, String key, String iv, String encodingFormat) throws Exception {
//    initialize();

        //被加密的資料
        byte[] dataByte = Base64.decodeBase64(data);
        //加密祕鑰
        byte[] keyByte = Base64.decodeBase64(key);
        //偏移量
        byte[] ivByte = Base64.decodeBase64(iv);


        try {
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");

            SecretKeySpec spec = new SecretKeySpec(keyByte, "AES");

            AlgorithmParameters parameters = AlgorithmParameters.getInstance("AES");
            parameters.init(new IvParameterSpec(ivByte));

            cipher.init(Cipher.DECRYPT_MODE, spec, parameters);// 初始化

            byte[] resultByte = cipher.doFinal(dataByte);
            if (null != resultByte && resultByte.length > 0) {
                String result = new String(resultByte, encodingFormat);
                return result;
            }
            return null;
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (NoSuchPaddingException e) {
            e.printStackTrace();
        } catch (InvalidParameterSpecException e) {
            e.printStackTrace();
        } catch (InvalidKeyException e) {
            e.printStackTrace();
        } catch (InvalidAlgorithmParameterException e) {
            e.printStackTrace();
        } catch (IllegalBlockSizeException e) {
            e.printStackTrace();
        } catch (BadPaddingException e) {
            e.printStackTrace();
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }

        return null;
    }

}

注意這裡解密完成後如果需要進行token認證的系統,理論上是可以通過JWT利用傳過來的三個資料造出token並儲存到資料庫從而進行認證的。有shiro的系統也是可以在config裡面進行遮蔽操作的。

大概完成周期一個禮拜,給自己定個小目標。