前言

微信小程序登錄-時(shí)序圖jpg.jpg
- 微信小程序相關(guān)文檔
UniApp前端
- 頁(yè)面初始化完畢時(shí),調(diào)用uni.login(),獲取到微信小程序的登錄憑證,然后保存到一個(gè)變量(坑,如果不提前獲取,例如點(diǎn)擊登錄時(shí)獲取,有時(shí)候會(huì)失敗,所以就只能提前獲取了)
- 頁(yè)面放置一個(gè)button按鈕,并需要設(shè)置
open-type屬性為getPhoneNumber,則表示該按鈕是用于獲取用戶手機(jī)號(hào)的,然后監(jiān)聽(tīng)getphonenumber事件,當(dāng)用戶允許獲取手機(jī)號(hào)后,獲取到手機(jī)號(hào)信息時(shí)回調(diào) - 手機(jī)號(hào)信息中,會(huì)包含以下參數(shù)
-
code,獲取手機(jī)號(hào)的憑證,和uni.login返回的code不一樣,登錄的code只能用于登錄,而這里的code只能用于換取用戶手機(jī)號(hào)。后端定義登錄的code為code,而獲取手機(jī)號(hào)的code為phoneCode -
encryptedData,加密信息 -
iv,加密向量
-
- 調(diào)用我們自己后端的
login登錄方法,把登錄的code和獲取手機(jī)號(hào)的code,一起傳過(guò)去,如果encryptedData和iv后端有需要的話,也一起傳
前端頁(yè)面
<template>
<view class="viewport">
<view class="logo">
<image src=".static/images/logo_icon.png"></image>
</view>
<view class="login">
<!-- #ifdef MP-WEIXIN -->
<button style="margin-bottom: 20rpx;" class="button phone" open-type="getPhoneNumber"
@getphonenumber="onGetPhone">
<text class="icon icon-phone"></text>
微信手機(jī)號(hào)快捷登錄
</button>
<!-- #endif -->
<view class="extra">
<view class="caption">
<text>其它登錄方式</text>
</view>
<view class="options">
<button>
<text class="icon icon-weixin">微信</text>
</button>
<button>
<text class="icon icon-phone">手機(jī)</text>
</button>
<button>
<text class="icon icon-mail">郵箱</text>
</button>
</view>
</view>
<view class="tips">登錄/注冊(cè)即視為你同意《服務(wù)條款》和《隱私協(xié)議》</view>
</view>
</view>
</template>
<script setup>
import { loginByWXAPI } from '@/api/profile'
import { useUserStore } from '@/store'
import { onLoad } from '@dcloudio/uni-app'
// 創(chuàng)建用戶信息Store
const userStore = useUserStore()
const { saveProfile } = userStore
// 微信臨時(shí)憑證code(動(dòng)態(tài)令牌,code來(lái)?yè)Q取用戶手機(jī)號(hào)。每個(gè)code有效期為5分鐘,且只能使用一次)
let loginCode = ""
// fix:由于獲取code的時(shí)機(jī)和getPhoneNumber,同時(shí)以 async await 同步形式執(zhí)行,會(huì)有一定概率會(huì)登錄失敗,所以需要將獲取code的時(shí)機(jī)提前,來(lái)解決這個(gè)問(wèn)題
onLoad(async () => {
// 小程序端,才執(zhí)行小程序登錄
// #ifndef H5
// 發(fā)起微信登錄
const result = await uni.login()
console.log(result);
loginCode = result.code
console.log(`獲取登錄的code成功:${loginCode}`);
// #endif
})
// 微信手機(jī)號(hào)快捷登錄
const onGetPhone = async (e) => {
console.log(e);
// 獲取返回的用戶加密數(shù)據(jù)和解密需要使用的iv
const { code: phoneCode, encryptedData, iv, errMsg } = e.detail;
if (!encryptedData || !iv) {
uni.showToast({
title: `登錄失?。?{errMsg}`,
icon: 'none'
})
return
}
// 調(diào)用自己后端的登錄接口,將encryptedData、iv、code,傳給后端
const result = await loginByWXAPI({
encryptedData: encryptedData,
iv: iv,
// 登錄的臨時(shí)憑證
code: loginCode,
// 獲取手機(jī)號(hào)的臨時(shí)憑證
phoneCode: phoneCode
})
console.log(result);
// 登錄成功,保存用戶信息到pinia中
saveProfile(result.result)
}
</script>
<style lang="scss">
</style>
請(qǐng)求API
- 封裝業(yè)務(wù)接口,并提供js請(qǐng)求方法
import http from "@/utils/http";
/**
* 小程序登錄
*
* @param {string} encryptedData 加密的手機(jī)號(hào)信息 getphonenumber事件回調(diào)中獲取
* @param {string} iv 加密相關(guān) getphonenumber事件回調(diào)中獲取
* @param {string} code 通過(guò) wx.login() 獲取
*/
export const loginByWXAPI = (data) => {
return http({
url: "/customer/user/login",
method: "POST",
data: {
// 用戶加密數(shù)據(jù)
encryptedData: data.encryptedData,
// 解密使用的向量
iv: data.iv,
// 登錄臨時(shí)憑證
code: data.code,
// 獲取手機(jī)號(hào)的臨時(shí)憑證
phoneCode: data.phoneCode
}
});
};
請(qǐng)求工具類(lèi)
- 封裝
UniApp的請(qǐng)求攔截器和響應(yīng)攔截器 - 在請(qǐng)求攔截器中,統(tǒng)一將token放到請(qǐng)求頭中
- 在響應(yīng)攔截器中,進(jìn)行數(shù)據(jù)剝離,以及處理401的HTTP狀態(tài)碼,處理登錄態(tài)失效,跳轉(zhuǎn)到登錄頁(yè),清除本地保存的token信息
- 在請(qǐng)求前和請(qǐng)求后,展示Loading,以及錯(cuò)誤信息的Toast
import { useUserStore } from '@/store'
// 基地址
const baseURL = "https://xxx.api.com";
// 發(fā)請(qǐng)求前觸發(fā)-等價(jià)于請(qǐng)求攔截器
const interceptor = {
invoke(args) {
// 獲取用戶信息Store
const userStore = useUserStore()
// 統(tǒng)一顯示Loading
uni.showLoading({ title: "拼命加載中..." });
// 通用參數(shù)
const commonParamsHeader = {}
// 不是https開(kāi)頭,則將URL拼接上基地址(也就是,如果寫(xiě)全了地址,則不拼接基地址了)
if (!args.url.startsWith("https")) {
args.url = baseURL + args.url;
}
// 設(shè)置token
const { token } = userStore
if (token) {
commonParamsHeader.Authorization = token
}
// 設(shè)置請(qǐng)求頭
args.header = {
// 保留原本的 header
...args.header,
// 添加小程序端調(diào)用標(biāo)識(shí)
"source-client": "miniapp",
// 添加通用參數(shù)
...commonParamsHeader
};
},
complete(res) {
// 請(qǐng)求完成,隱藏Loading
uni.hideLoading();
},
};
// 請(qǐng)求攔截器
uni.addInterceptor("request", interceptor);
// 文件上傳攔截器
uni.addInterceptor("uploadFile", interceptor);
// 發(fā)請(qǐng)求后-等價(jià)于響應(yīng)攔截器
const http = async (options) => {
// 請(qǐng)求返回結(jié)果,返回一個(gè)數(shù)組,第一個(gè)參數(shù):錯(cuò)誤信息,第二個(gè)參數(shù):接口返回的結(jié)果
const res = await uni.request(options);
// HTTP響應(yīng)狀態(tài)碼
const { statusCode } = res
// token過(guò)期,跳轉(zhuǎn)到登錄頁(yè)面
if (statusCode === 401) {
uni.navigateTo({ url: "/pages/login/login" });
return
}
// 請(qǐng)求成功
if (statusCode >= 200 && statusCode < 300) {
return res.data;
} else {
// 請(qǐng)求失敗,提示錯(cuò)誤信息
uni.showToast({
title: res.data.message,
icon: 'none'
})
return Promise.reject(new Error(res))
}
};
// 導(dǎo)出
export default http;
Java后端
客戶表實(shí)體類(lèi)
/**
* 用戶表
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Member extends BaseEntity {
/**
* 手機(jī)號(hào)
*/
private String phone;
/**
* 名稱(chēng)
*/
private String name;
/**
* 頭像
*/
private String avatar;
/**
* 微信OpenID
*/
private String openId;
/**
* 性別
*/
private Integer gender;
}
客戶登錄Dto
/**
* C端用戶登錄Dto
*/
@Data
public class UserLoginRequestDto {
@ApiModelProperty("微信昵稱(chēng)")
private String nickName;
@ApiModelProperty("登錄臨時(shí)憑證")
private String code;
@ApiModelProperty("手機(jī)號(hào)臨時(shí)憑證")
private String phoneCode;
}
客戶登錄Vo
/**
* C端用戶登錄Vo
*/
@Data
@Builder
@ApiModel(value = "登錄對(duì)象")
public class LoginVo {
@ApiModelProperty(value = "JWT token")
private String token;
@ApiModelProperty(value = "昵稱(chēng)")
private String nickName;
}
CustomerUserController,登錄接口
@Slf4j
@Api(tags = "客戶管理")
@RestController
@RequestMapping("/customer/user")
public class CustomerUserController {
@Autowired
private MemberService memberService;
/**
* C端用戶登錄--微信登錄
*
* @param userLoginRequestDto 用戶登錄信息
* @return 登錄結(jié)果
*/
@PostMapping("/login")
@ApiOperation("登錄")
public ResponseResult<LoginVo> login(@RequestBody UserLoginRequestDto userLoginRequestDto) {
LoginVo loginVo = memberService.login(userLoginRequestDto);
return ResponseResult.success(loginVo);
}
}
MemberService,客戶業(yè)務(wù)層接口
/**
* C端用戶業(yè)務(wù)層接口
*/
public interface MemberService {
/**
* 微信小程序端登錄
*/
LoginVo login(UserLoginRequestDto userLoginRequestDto);
}
MemberServiceImpl,客戶業(yè)務(wù)層實(shí)現(xiàn)類(lèi)
- 通過(guò)前端傳過(guò)來(lái)的登錄臨時(shí)憑證
code,換取用戶的openId,也就是小程序登錄 - 以及通過(guò)獲取手機(jī)號(hào)的臨時(shí)憑證
phoneCode,換取用戶的手機(jī)號(hào) - 通過(guò)
openId查詢(xún)用戶是否存在- 不存在,則為第一次登錄,保存用戶的手機(jī)號(hào)和openId到數(shù)據(jù)庫(kù)表中
- 存在,則判斷用戶手機(jī)號(hào)是否有變更,有則更新到數(shù)據(jù)庫(kù)表中
- 最后,通過(guò)JWT生成用戶的token,并和用戶信息一起返回給前端
- 與微信的API交互,是通過(guò)HTTP協(xié)議,都封裝到了
WechatService中
/**
* C端用戶業(yè)務(wù)層實(shí)現(xiàn)類(lèi)
*/
@Service
public class MemberServiceImpl implements MemberService {
@Autowired
private WechatService wechatService;
@Autowired
private MemberMapper memberMapper;
/**
* JWT配置信息
*/
@Autowired
private JwtTokenManagerProperties jwtTokenManagerProperties;
/**
* 用戶昵稱(chēng),隨機(jī)前綴
*/
private static final List<String> DEFAULT_NICKNAME_PREFIX = Lists.newArrayList(
"生活更美好",
"大桔大利",
"日富一日",
"好柿開(kāi)花",
"柿柿如意",
"一椰暴富",
"大柚所為",
"楊梅吐氣",
"天生荔枝"
);
/**
* 小程序端登錄
*/
@Override
public LoginVo login(UserLoginRequestDto userLoginRequestDto) {
// 調(diào)用微信API,根據(jù)前端傳過(guò)來(lái)的code(臨時(shí)憑證),獲取openId
String openId = wechatService.getOpenid(userLoginRequestDto.getCode());
// 調(diào)用微信API,獲取用戶綁定的手機(jī)號(hào)
String phone = wechatService.getPhoneNumber(userLoginRequestDto.getPhoneCode());
// 根據(jù)openId,查詢(xún)用戶信息
Member member = memberMapper.getByOpenId(openId);
// 如果用戶為空信息,則為新用戶,則創(chuàng)建新用戶信息,并設(shè)置openId
if (ObjectUtil.isEmpty(member)) {
member = Member.builder()
.openId(openId)
.build();
}
// 保存或修改用戶
saveOrUpdate(member, phone);
// 創(chuàng)建token
String token = createMemberToken(member);
// 返回用戶信息
return LoginVo.builder()
.token(token)
.nickName(member.getName())
.build();
}
/**
* 保存或修改客戶
*
* @param member 數(shù)據(jù)庫(kù)中的用戶信息
* @param phoneNumber 手機(jī)號(hào)
*/
private void saveOrUpdate(Member member, String phoneNumber) {
// 如果從微信取到的手機(jī)號(hào),和數(shù)據(jù)庫(kù)中保存的手機(jī)號(hào)不一樣,那么更新手機(jī)號(hào)
if (ObjectUtil.notEqual(phoneNumber, member.getPhone())) {
member.setPhone(phoneNumber);
}
// id存在,則更新用戶信息
if (member.getId() != null) {
memberMapper.updateMember(member);
} else {
// 隨機(jī)組裝昵稱(chēng),詞組+手機(jī)號(hào)后四位
int randomIndex = (int) (Math.random() * DEFAULT_NICKNAME_PREFIX.size());
String nickName = DEFAULT_NICKNAME_PREFIX.get(randomIndex)
+ StringUtils.substring(member.getPhone(), 7);
member.setName(nickName);
// id不存在,則為新用戶,創(chuàng)建用戶
memberMapper.addMember(member);
}
}
/**
* 常見(jiàn)用戶token
*/
private String createMemberToken(Member member) {
// token保存的信息
Map<String, Object> claims = new HashMap<>();
// 用戶Id
claims.put(Constants.JWT_USERID, member.getId());
// 用戶名稱(chēng)
claims.put(Constants.JWT_USERNAME, member.getName());
String secretKey = jwtTokenManagerProperties.getBase64EncodedSecretKey();
// 過(guò)期時(shí)間
int dateOffset = jwtTokenManagerProperties.getTtl();
// 創(chuàng)建token
return JwtUtil.createJWT(
secretKey,
dateOffset,
claims
);
}
}
MemberMapper,客戶Mapper接口
- 定義客戶表的增、刪、查、改方法
/**
* C端用戶Mapper
*/
@Mapper
public interface MemberMapper {
/**
* 根據(jù)微信OpenId,查詢(xún)用戶信息
*/
Member getByOpenId(String openId);
/**
* 添加用戶信息
*/
void addMember(Member member);
/**
* 更新用戶信息
*/
void updateMember(Member member);
}
MemberMapper.xml,客戶Mapper的xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.zzyl.mapper.MemberMapper">
<resultMap id="BaseResultMap" type="com.zzyl.entity.Member">
<id column="id" property="id"/>
<result column="phone" property="phone"/>
<result column="name" property="name"/>
<result column="avatar" property="avatar"/>
<result column="open_id" property="openId"/>
<result column="gender" property="gender"/>
<result column="create_by" property="createBy"/>
<result column="update_by" property="updateBy"/>
<result column="remark" property="remark"/>
<result column="create_time" property="createTime"/>
<result column="update_time" property="updateTime"/>
</resultMap>
<select id="getByOpenId" resultType="com.zzyl.entity.Member">
SELECT *
FROM member
WHERE open_id = #{openId}
</select>
<insert id="addMember" parameterType="com.zzyl.entity.Member" keyProperty="id" useGeneratedKeys="true">
INSERT INTO member (phone, name, avatar, open_id, gender, create_by, create_time)
VALUES (#{phone}, #{name}, #{avatar}, #{openId}, #{gender}, #{createBy}, #{createTime})
</insert>
<update id="updateMember" parameterType="com.zzyl.entity.Member">
UPDATE member
SET phone = #{phone},
name = #{name},
avatar = #{avatar},
open_id = #{openId},
gender = #{gender},
update_by = #{updateBy},
update_time = #{updateTime}
WHERE id = #{id}
</update>
</mapper>
微信相關(guān)業(yè)務(wù)
自定義yml屬性
# 自定義屬性配置
zzyl:
# 微信小程序配置
wechat:
# 微信小程序的appid和appSecret
appId: xxx
appSecret: xxx
微信配置類(lèi)
- 讀取并映射,yml中的自定義屬性到Java類(lèi)的屬性上
/**
* 微信配置的Properties屬性類(lèi)
*/
@Setter
@Getter
@NoArgsConstructor
@ToString
@Configuration
@ConfigurationProperties(prefix = "zzyl.wechat")
public class WeChatConfigProperties {
/**
* 微信小程序的AppId
*/
private String appId;
/**
* 微信小程序的AppSecret
*/
private String appSecret;
}
WechatService,微信業(yè)務(wù)層接口
/**
* 微信業(yè)務(wù)層接口
*/
public interface WechatService {
/**
* 獲取openid
*
* @param code 登錄憑證
*/
String getOpenid(String code);
/**
* 獲取手機(jī)號(hào)
*
* @param phoneCode 手機(jī)號(hào)憑證
*/
String getPhoneNumber(String phoneCode);
}
WechatServiceImpl,微信業(yè)務(wù)層實(shí)現(xiàn)類(lèi)
- 主要有3個(gè)接口需要我們調(diào)用,分別為:
-
jscode2session,通過(guò)登錄臨時(shí)憑證code,換取微信小程序的openId,也就是小程序登錄 -
token,獲取微信小程序的AccessToken,獲取用戶手機(jī)號(hào)請(qǐng)求中,需要添加該參數(shù) -
getuserphonenumber,通過(guò)獲取手機(jī)號(hào)臨時(shí)憑證phoneCode,獲取用戶的手機(jī)號(hào)
-
/**
* 微信業(yè)務(wù)層實(shí)現(xiàn)類(lèi)
*/
@Service
public class WechatServiceImpl implements WechatService {
/**
* 使用code,獲取OpenId
*/
private static final String CODE_2_SESSION_URL = "https://api.weixin.qq.com/sns/jscode2session?grant_type=authorization_code";
/**
* 獲取token
*/
private static final String TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential";
/**
* 獲取手機(jī)號(hào)
*/
private static final String PHONE_REQUEST_URL = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=";
/**
* 微信配置信息
*/
@Autowired
private WeChatConfigProperties weChatConfigProperties;
/**
* 獲取openid
*
* @param code 微信小程序的登錄臨時(shí)憑證
*/
@Override
public String getOpenid(String code) {
// 獲取公共參數(shù)
Map<String, Object> requestUrlParam = getAppConfig();
// 登錄時(shí)獲取的 code,可通過(guò)wx.login獲取
requestUrlParam.put("js_code", code);
// 授權(quán)類(lèi)型
requestUrlParam.put("grant_type", "authorization_code");
// 發(fā)起請(qǐng)求,用code換取openId
String result = HttpUtil.get(CODE_2_SESSION_URL, requestUrlParam);
JSONObject jsonObject = JSONUtil.parseObj(result);
// 若code不正確,則獲取不到openid,那么響應(yīng)失敗
Integer errCode = jsonObject.getInt("errcode");
if (errCode != null && errCode != 0) {
// 錯(cuò)誤信息
String errMsg = jsonObject.getStr("errmsg");
throw new RuntimeException(errMsg);
}
return jsonObject.getStr("openid");
}
/**
* 獲取手機(jī)號(hào)
*
* @param phoneCode 手機(jī)號(hào)憑證
*/
@Override
public String getPhoneNumber(String phoneCode) {
// 獲取微信的AccessToken
String accessToken = getAccessToken();
// 將token,拼接到請(qǐng)求路徑中
String url = PHONE_REQUEST_URL + accessToken;
Map<String, Object> param = new HashMap<>();
// 手機(jī)號(hào)臨時(shí)憑證
param.put("code", phoneCode);
// 參數(shù)要轉(zhuǎn)成json,不能直接傳Map,否則會(huì)返回失敗,提示請(qǐng)求參數(shù)格式不正確
String json = JSONUtil.toJsonStr(param);
// 發(fā)起請(qǐng)求
String result = HttpUtil.post(url, json);
// 解析響應(yīng)
JSONObject jsonObject = JSONUtil.parseObj(result);
Integer errCode = jsonObject.getInt("errcode");
// 若code不正確,則獲取不到手機(jī)號(hào),那么響應(yīng)失敗
if (errCode != 0) {
// 錯(cuò)誤信息
String errMsg = jsonObject.getStr("errmsg");
throw new RuntimeException(errMsg);
}
/*
響應(yīng)示例:
{
"errcode":0,
"errmsg":"ok",
"phone_info": {
"phoneNumber":"xxxxxx",
"purePhoneNumber": "xxxxxx",
"countryCode": 86,
"watermark": {
"timestamp": 1637744274,
"appid": "xxxx"
}
}
}
*/
// 用戶的手機(jī)號(hào)信息
JSONObject phoneInfo = jsonObject.getJSONObject("phone_info");
// 獲取手機(jī)號(hào)
return phoneInfo.getStr("purePhoneNumber");
}
/**
* 獲取微信的AccessToken
*/
public String getAccessToken() {
// 獲取公共參數(shù)
Map<String, Object> requestUrlParam = getAppConfig();
// 授權(quán)類(lèi)型
requestUrlParam.put("grant_type", "client_credential");
// 發(fā)起請(qǐng)求
String result = HttpUtil.get(TOKEN_URL, requestUrlParam);
// 解析響應(yīng)
JSONObject jsonObject = JSONUtil.parseObj(result);
// 錯(cuò)誤碼
Integer errCode = jsonObject.getInt("errcode");
// 如果錯(cuò)誤,則返回錯(cuò)誤信息
if (errCode != null && errCode != 0) {
// 錯(cuò)誤信息
String errMsg = jsonObject.getStr("errmsg");
throw new RuntimeException(errMsg);
}
return jsonObject.getStr("access_token");
}
/**
* 獲取公共參數(shù)
*/
private Map<String, Object> getAppConfig() {
Map<String, Object> requestUrlParam = new HashMap<>();
requestUrlParam.put("appid", weChatConfigProperties.getAppId());
requestUrlParam.put("secret", weChatConfigProperties.getAppSecret());
return requestUrlParam;
}
}