整體流程如下:
- 1.獲取code
- 2.用步驟1獲取到的臨時code換取用戶唯一標識OpenId和會話密鑰 session_key
- 3.獲取用戶手機號,進行登錄
請求參數:
請求參數:
屬性 | 類型 | 默認值 | 必填 | 說明 |
---|---|---|---|---|
appid | string | 是 | 小程序 appId | |
secret | string | 是 | 小程序 appSecret | |
js_code | string | 是 | 登錄時獲取的 code | |
grant_type | string | 是 | 授權類型,此處只需填寫 authorization_code |
返回值:
屬性 | 類型 | 說明 |
---|---|---|
openid | string | 用戶唯一標識 |
session_key | string | 會話密鑰 |
unionid | string | 用戶在開放平台的唯一標識符,在滿足 UnionID 下發條件的情況下會返回,詳見 UnionID 機制說明。 |
errcode | number | 錯誤碼 |
errmsg | string | 錯誤信息 |
開發前准備(必須)
appid:wx4c1d53****7a671
appsecret:fc9f99c08dcb2a0****eb26376c65eae
小程序代碼:
login.js
// pages/login/login.wxml.js
const app = getApp();
Page({
/**
* 頁面的初始數據
*/
data: {
// 判斷小程序的API,回調,參數,組件等是否在當前版本可用。
canIUse: wx.canIUse('button.open-type.getPhoneNumber'),
wechat: '微信快捷登錄',
isShow: false
},
/**
* 生命周期函數--監聽頁面加載
*/
onLoad: function (options) {
// 從緩存中取手機號
console.log("獲取手機號!")
try {
var value = wx.getStorageSync('phoneNumber')
if (value) { // 說明已登錄 跳轉 頁面
console.log("獲取緩存:"+value)
wx.navigateTo({
url: '../login?param=' + value
})
}else{// 未登錄 顯示 微信授權頁面
this.setData({
isShow: true
})
}
} catch (e) {
}
// 解決第一次獲取手機號失敗問題
wx.login({
success: res => {
if(res.code){
console.log("code->", res.code)
}
}
})
},
// 0.獲取手機號授權
getPhoneNumber: function(e) {
// 用戶拒絕授權
if(e.detail.errMsg == "getPhoneNumber:fail user deny") {
wx.showToast({
icon: "none",
title: '請允許獲取手機號,否則功能不可用!',
})
return
}
// console.log("e->detail", e.detail)
/// 用戶允許授權
// console.log("iv->", e.detail.iv); //包括敏感數據在內的完整用戶信息的加密數據,需要解密
// console.log("encryptedData->", e.detail.encryptedData); //加密算法的初始向量,解密需要用到
/// 獲取手機號
// 1.獲取臨時登錄憑證code
wx.login({
success: res => {
if(res.code){
this.code = res.code;
console.log("code->", res.code)
console.log("encryptedData->", encodeURIComponent(e.detail.encryptedData))
console.log("iv->", e.detail.iv)
this.getSessionKey(res.code, e.detail.encryptedData, e.detail.iv);
}
}
})
},
// 2.訪問登錄憑證校驗接口獲取session_key(后續改成后台實現)
getSessionKey: function(js_code, encryptedData, iv) {
// 3. 解密獲取手機號
wx.request({
url: 'http://localhost:8082/wechat/appletLogin',
data: {
'encryptedData': encodeURIComponent(encryptedData),//需要進行編碼
'iv': iv,
'jscode': js_code
},
method: 'POST',
header: {
'content-type': 'application/json'
}, // 設置請求的 header
success: function(data2) {
var data = data2.data
console.log(data)
if(data.resultCode == 200) {
if(data.data.user.phone==undefined){
// 獲取手機號失敗 跳轉到 常規 用戶登錄頁面(通過webview)
wx.navigateTo({
url: '用戶登錄頁'
})
return
}
// 存儲數據到緩存
wx.setStorage({
key:"phoneNumber",
data:data.data.user.phone
})
// 4.跳轉頁面
}
},
fail: function(err) {
console.log(err);
wx.showToast({
icon: "none",
title: '獲取手機號失敗,請重試!',
})
}
})
},
/**
* 生命周期函數--監聽頁面初次渲染完成
*/
onReady: function () {
},
/**
* 生命周期函數--監聽頁面顯示
*/
onShow: function () {
},
/**
* 生命周期函數--監聽頁面隱藏
*/
onHide: function () {
},
/**
* 生命周期函數--監聽頁面卸載
*/
onUnload: function () {
},
/**
* 頁面相關事件處理函數--監聽用戶下拉動作
*/
onPullDownRefresh: function () {
},
/**
* 頁面上拉觸底事件的處理函數
*/
onReachBottom: function () {
},
/**
* 用戶點擊右上角分享
*/
onShareAppMessage: function () {
}
})
login.json
{
"usingComponents": {}
}
login.wxml
<!--pages/login.wxml-->
<view class="modal-mask" catchtouchmove="preventTouchMove" wx:if="{{isShow}}"></view>
<view class="modal-dialog" wx:if="{{isShow}}">
<view class="modal-content">
<view><image src='../images/show.png' class='show'></image></view>
<!-- <view>綁定手機號</view>
<view>請先綁定手機號在進行此操作</view> -->
<button class="show" type="primary" lang="zh_CN"
open-type='getPhoneNumber' bindgetphonenumber="getPhoneNumber" >
微信用戶一鍵登錄
</button>
</view>
</view>
login.wxss
/* pages/login/login.wxml.wxss */
.show{
display: block;
border-radius: 8rpx;
margin: 20rpx 20rpx 20rpx 20rpx;
font-size: 35rpx;
}
.container{
position: fixed; /*關鍵屬性,設置為fixed*/
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
后端代碼
WxLoginController.java
@RestController
@RequestMapping("/wechat")
public class WxLoginController {
private Logger logger = LoggerFactory.getLogger(WxLoginController.class);
@Autowired
UserDao userDao;
@Value("${wechat.appid}")
private String appid;
@Value("${wechat.appsecret}")
private String appsecret;
private Map<String, Object> userMap = new HashMap<>();
@GetMapping("/appletLogin")
public ResultBean appletLogin(String encryptedData, String iv, String jscode) {
System.out.println(encryptedData);
ResultBean resultBean = new ResultBean();
// jscode
if (StringUtils.isEmpty(jscode)) {
resultBean.setResultCode(-10001);
resultBean.setResultMessage("鑒權失敗!");
return resultBean;
}
// sign
if (StringUtils.isEmpty(encryptedData)) {
resultBean.setResultCode(-10001);
resultBean.setResultMessage("鑒權失敗!");
return resultBean;
}
if(StringUtils.isBlank(iv)) {
resultBean.setResultCode(-10002);
resultBean.setResultMessage("鑒權失敗!");
return resultBean;
}
// 創建Httpclient對象
CloseableHttpClient httpclient = HttpClients.createDefault();
String resultString = "";
CloseableHttpResponse response = null;
String url =WxContants.url.replace(WxContants.APPID, appid).
replace(WxContants.SECRET, appsecret).replace(WxContants.JSCODE, jscode);
try {
// 創建uri
URIBuilder builder = new URIBuilder(url);
URI uri = builder.build();
// 創建http GET請求
HttpGet httpGet = new HttpGet(uri);
// 執行請求
response = httpclient.execute(httpGet);
// 判斷返回狀態是否為200
if (response.getStatusLine().getStatusCode() == 200) {
resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
}
} catch (Exception e) {
e.printStackTrace();
}
// 解析json
JSONObject jsonObject = (JSONObject) JSONObject.parse(resultString);
String session_key = jsonObject.getString("session_key");
String openid = jsonObject.getString("openid");
System.out.println("session_key:"+session_key + ",openid:"+openid);
if (StringUtils.isEmpty(session_key)) {
resultBean.setResultCode(-10003);
resultBean.setResultMessage("獲取sessionkey失敗!");
return resultBean;
}
// 解碼
try {
encryptedData = URLDecoder.decode(encryptedData,"UTF-8");
} catch (UnsupportedEncodingException e) {
logger.error("encryptedData,decode失敗!", e);
resultBean.setResultCode(-10003);
resultBean.setResultMessage("encryptedData,decode失敗!");
return resultBean;
}
String decryptData = WechatDecryptDataUtil.decryptData(encryptedData, session_key, iv);
JSONObject userInfo = JSONObject.parseObject(decryptData);
// 存入redis+數據庫,判斷是否存在redis中,存在直接從redis返回
// 本地使用Map模擬
if (!userMap.containsKey(openid)) {
// 判斷用戶是否存在系統中
String phoneNumber = userInfo.getString("phoneNumber");
User user = userDao.findByPhone(phoneNumber);
if (user == null) {
resultBean.setResultCode(200);
resultBean.setResultMessage("此用戶不存在,請先注冊!");
return resultBean;
}
user.setOpenId(openid);
userDao.save(user);
userMap.put(openid, user);
}
User user = (User) userMap.get(openid);
resultBean.setResultCode(200);
resultBean.setResultMessage("成功!");
resultBean.setData(user);
return resultBean;
}
}
UserDao.java
public interface UserDao extends JpaRepository<User, Long> {
User findByPhone(String phone);
User findByOpenId(String openId);
}
User.java
@Entity
@Table(name = "user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String openId;
private String phone;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getOpenId() {
return openId;
}
public void setOpenId(String openId) {
this.openId = openId;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
}
WechatDecryptDataUtil.java
public class WechatDecryptDataUtil {
private static Logger logger = LogManager.getLogger(WechatDecryptDataUtil.class);
public static void main(String[] args) {
String result = decryptData(
"BLY05rL1qLzGdqn+m4s4KfnM9CbMCm2sTZmgIcUJUpoOVeZkKKYh06ATVm2BgX8HUsH1a93811fhwr70MTh2Pk2qw9rPBzvos3husUJdVfaZgBk3Afp6wNFS0/kcoyt+7q2eMrIHMe6wkc4J0hgbbkxdUwZag/pyKXwe4pUzSQfta7dfBHy3DLu+REvwHiDNfvI3KJbZ0l8/9vRjfSmT4Q==",
"C9Z59YjFkCDT8+9NQ3OxcA==",
"oOWTpbZ80evG/GQqRdUu3w=="
);
System.out.println("result = " + result);
}
public synchronized static String decryptData(String encryptDataB64, String sessionKeyB64, String ivB64) {
String res = null;
try {
res = new String(
decryptOfDiyIV(
Base64.decode(encryptDataB64),
Base64.decode(sessionKeyB64),
Base64.decode(ivB64)
)
);
} catch (Exception e) {
logger.error("encryptDataB64:"+encryptDataB64+"\n"+"sessionKeyB64:"+sessionKeyB64+"\n"+"ivB64:"+ivB64);
}
return res;
}
private static final String KEY_ALGORITHM = "AES";
private static final String ALGORITHM_STR = "AES/CBC/PKCS7Padding";
private static Key key;
private static Cipher cipher;
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(ALGORITHM_STR, "BC");
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 解密方法
*
* @param encryptedData 要解密的字符串
* @param keyBytes 解密密鑰
* @param ivs 自定義對稱解密算法初始向量 iv
* @return 解密后的字節數組
*/
private static byte[] decryptOfDiyIV(byte[] encryptedData, byte[] keyBytes, byte[] ivs) {
byte[] encryptedText = null;
init(keyBytes);
try {
cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(ivs));
encryptedText = cipher.doFinal(encryptedData);
} catch (Exception e) {
e.printStackTrace();
}
return encryptedText;
}
}
WxContants.java
public interface WxContants {
String APPID = "APPID";
String SECRET = "SECRET";
String JSCODE = "JSCODE";
String TRADE_TYPE = "JSAPI";
String url = "https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code";
}