前言
UniApp前端
- 页面初始化完毕时,调用uni.login(),获取到微信小程序的登录凭证,然后保存到一个变量(坑,如果不提前获取,例如点击登录时获取,有时候会失败,所以就只能提前获取了)
- 页面放置一个button按钮,并需要设置
open-type
属性为getPhoneNumber
,则表示该按钮是用于获取用户手机号的,然后监听getphonenumber
事件,当用户允许获取手机号后,获取到手机号信息时回调
- 手机号信息中,会包含以下参数
-
code
,获取手机号的凭证,和uni.login
返回的code
不一样,登录的code只能用于登录,而这里的code
只能用于换取用户手机号。后端定义登录的code为code
,而获取手机号的code
为phoneCode
-
encryptedData
,加密信息
-
iv
,加密向量
- 调用我们自己后端的
login
登录方法,把登录的code
和获取手机号的code
,一起传过去,如果encryptedData
和iv
后端有需要的话,也一起传
前端页面
<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>
微信手机号快捷登录
</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">手机</text>
</button>
<button>
<text class="icon icon-mail">邮箱</text>
</button>
</view>
</view>
<view class="tips">登录/注册即视为你同意《服务条款》和《隐私协议》</view>
</view>
</view>
</template>
<script setup>
import { loginByWXAPI } from '@/api/profile'
import { useUserStore } from '@/store'
import { onLoad } from '@dcloudio/uni-app'
// 创建用户信息Store
const userStore = useUserStore()
const { saveProfile } = userStore
// 微信临时凭证code(动态令牌,code来换取用户手机号。每个code有效期为5分钟,且只能使用一次)
let loginCode = ""
// fix:由于获取code的时机和getPhoneNumber,同时以 async await 同步形式执行,会有一定概率会登录失败,所以需要将获取code的时机提前,来解决这个问题
onLoad(async () => {
// 小程序端,才执行小程序登录
// #ifndef H5
// 发起微信登录
const result = await uni.login()
console.log(result);
loginCode = result.code
console.log(`获取登录的code成功:${loginCode}`);
// #endif
})
// 微信手机号快捷登录
const onGetPhone = async (e) => {
console.log(e);
// 获取返回的用户加密数据和解密需要使用的iv
const { code: phoneCode, encryptedData, iv, errMsg } = e.detail;
if (!encryptedData || !iv) {
uni.showToast({
title: `登录失败:${errMsg}`,
icon: 'none'
})
return
}
// 调用自己后端的登录接口,将encryptedData、iv、code,传给后端
const result = await loginByWXAPI({
encryptedData: encryptedData,
iv: iv,
// 登录的临时凭证
code: loginCode,
// 获取手机号的临时凭证
phoneCode: phoneCode
})
console.log(result);
// 登录成功,保存用户信息到pinia中
saveProfile(result.result)
}
</script>
<style lang="scss">
</style>
请求API
import http from "@/utils/http";
/**
* 小程序登录
*
* @param {string} encryptedData 加密的手机号信息 getphonenumber事件回调中获取
* @param {string} iv 加密相关 getphonenumber事件回调中获取
* @param {string} code 通过 wx.login() 获取
*/
export const loginByWXAPI = (data) => {
return http({
url: "/customer/user/login",
method: "POST",
data: {
// 用户加密数据
encryptedData: data.encryptedData,
// 解密使用的向量
iv: data.iv,
// 登录临时凭证
code: data.code,
// 获取手机号的临时凭证
phoneCode: data.phoneCode
}
});
};
请求工具类
- 封装
UniApp
的请求拦截器和响应拦截器
- 在请求拦截器中,统一将token放到请求头中
- 在响应拦截器中,进行数据剥离,以及处理401的HTTP状态码,处理登录态失效,跳转到登录页,清除本地保存的token信息
- 在请求前和请求后,展示Loading,以及错误信息的Toast
import { useUserStore } from '@/store'
// 基地址
const baseURL = "https://xxx.api.com";
// 发请求前触发-等价于请求拦截器
const interceptor = {
invoke(args) {
// 获取用户信息Store
const userStore = useUserStore()
// 统一显示Loading
uni.showLoading({ title: "拼命加载中..." });
// 通用参数
const commonParamsHeader = {}
// 不是https开头,则将URL拼接上基地址(也就是,如果写全了地址,则不拼接基地址了)
if (!args.url.startsWith("https")) {
args.url = baseURL + args.url;
}
// 设置token
const { token } = userStore
if (token) {
commonParamsHeader.Authorization = token
}
// 设置请求头
args.header = {
// 保留原本的 header
...args.header,
// 添加小程序端调用标识
"source-client": "miniapp",
// 添加通用参数
...commonParamsHeader
};
},
complete(res) {
// 请求完成,隐藏Loading
uni.hideLoading();
},
};
// 请求拦截器
uni.addInterceptor("request", interceptor);
// 文件上传拦截器
uni.addInterceptor("uploadFile", interceptor);
// 发请求后-等价于响应拦截器
const http = async (options) => {
// 请求返回结果,返回一个数组,第一个参数:错误信息,第二个参数:接口返回的结果
const res = await uni.request(options);
// HTTP响应状态码
const { statusCode } = res
// token过期,跳转到登录页面
if (statusCode === 401) {
uni.navigateTo({ url: "/pages/login/login" });
return
}
// 请求成功
if (statusCode >= 200 && statusCode < 300) {
return res.data;
} else {
// 请求失败,提示错误信息
uni.showToast({
title: res.data.message,
icon: 'none'
})
return Promise.reject(new Error(res))
}
};
// 导出
export default http;
Java后端
客户表实体类
/**
* 用户表
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Member extends BaseEntity {
/**
* 手机号
*/
private String phone;
/**
* 名称
*/
private String name;
/**
* 头像
*/
private String avatar;
/**
* 微信OpenID
*/
private String openId;
/**
* 性别
*/
private Integer gender;
}
客户登录Dto
/**
* C端用户登录Dto
*/
@Data
public class UserLoginRequestDto {
@ApiModelProperty("微信昵称")
private String nickName;
@ApiModelProperty("登录临时凭证")
private String code;
@ApiModelProperty("手机号临时凭证")
private String phoneCode;
}
客户登录Vo
/**
* C端用户登录Vo
*/
@Data
@Builder
@ApiModel(value = "登录对象")
public class LoginVo {
@ApiModelProperty(value = "JWT token")
private String token;
@ApiModelProperty(value = "昵称")
private String nickName;
}
CustomerUserController,登录接口
@Slf4j
@Api(tags = "客户管理")
@RestController
@RequestMapping("/customer/user")
public class CustomerUserController {
@Autowired
private MemberService memberService;
/**
* C端用户登录--微信登录
*
* @param userLoginRequestDto 用户登录信息
* @return 登录结果
*/
@PostMapping("/login")
@ApiOperation("登录")
public ResponseResult<LoginVo> login(@RequestBody UserLoginRequestDto userLoginRequestDto) {
LoginVo loginVo = memberService.login(userLoginRequestDto);
return ResponseResult.success(loginVo);
}
}
MemberService,客户业务层接口
/**
* C端用户业务层接口
*/
public interface MemberService {
/**
* 微信小程序端登录
*/
LoginVo login(UserLoginRequestDto userLoginRequestDto);
}
MemberServiceImpl,客户业务层实现类
- 通过前端传过来的登录临时凭证
code
,换取用户的openId
,也就是小程序登录
- 以及通过获取手机号的临时凭证
phoneCode
,换取用户的手机号
- 通过
openId
查询用户是否存在
- 不存在,则为第一次登录,保存用户的手机号和openId到数据库表中
- 存在,则判断用户手机号是否有变更,有则更新到数据库表中
- 最后,通过JWT生成用户的token,并和用户信息一起返回给前端
- 与微信的API交互,是通过HTTP协议,都封装到了
WechatService
中
/**
* C端用户业务层实现类
*/
@Service
public class MemberServiceImpl implements MemberService {
@Autowired
private WechatService wechatService;
@Autowired
private MemberMapper memberMapper;
/**
* JWT配置信息
*/
@Autowired
private JwtTokenManagerProperties jwtTokenManagerProperties;
/**
* 用户昵称,随机前缀
*/
private static final List<String> DEFAULT_NICKNAME_PREFIX = Lists.newArrayList(
"生活更美好",
"大桔大利",
"日富一日",
"好柿开花",
"柿柿如意",
"一椰暴富",
"大柚所为",
"杨梅吐气",
"天生荔枝"
);
/**
* 小程序端登录
*/
@Override
public LoginVo login(UserLoginRequestDto userLoginRequestDto) {
// 调用微信API,根据前端传过来的code(临时凭证),获取openId
String openId = wechatService.getOpenid(userLoginRequestDto.getCode());
// 调用微信API,获取用户绑定的手机号
String phone = wechatService.getPhoneNumber(userLoginRequestDto.getPhoneCode());
// 根据openId,查询用户信息
Member member = memberMapper.getByOpenId(openId);
// 如果用户为空信息,则为新用户,则创建新用户信息,并设置openId
if (ObjectUtil.isEmpty(member)) {
member = Member.builder()
.openId(openId)
.build();
}
// 保存或修改用户
saveOrUpdate(member, phone);
// 创建token
String token = createMemberToken(member);
// 返回用户信息
return LoginVo.builder()
.token(token)
.nickName(member.getName())
.build();
}
/**
* 保存或修改客户
*
* @param member 数据库中的用户信息
* @param phoneNumber 手机号
*/
private void saveOrUpdate(Member member, String phoneNumber) {
// 如果从微信取到的手机号,和数据库中保存的手机号不一样,那么更新手机号
if (ObjectUtil.notEqual(phoneNumber, member.getPhone())) {
member.setPhone(phoneNumber);
}
// id存在,则更新用户信息
if (member.getId() != null) {
memberMapper.updateMember(member);
} else {
// 随机组装昵称,词组+手机号后四位
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不存在,则为新用户,创建用户
memberMapper.addMember(member);
}
}
/**
* 常见用户token
*/
private String createMemberToken(Member member) {
// token保存的信息
Map<String, Object> claims = new HashMap<>();
// 用户Id
claims.put(Constants.JWT_USERID, member.getId());
// 用户名称
claims.put(Constants.JWT_USERNAME, member.getName());
String secretKey = jwtTokenManagerProperties.getBase64EncodedSecretKey();
// 过期时间
int dateOffset = jwtTokenManagerProperties.getTtl();
// 创建token
return JwtUtil.createJWT(
secretKey,
dateOffset,
claims
);
}
}
MemberMapper,客户Mapper接口
/**
* C端用户Mapper
*/
@Mapper
public interface MemberMapper {
/**
* 根据微信OpenId,查询用户信息
*/
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>
微信相关业务
自定义yml属性
# 自定义属性配置
zzyl:
# 微信小程序配置
wechat:
# 微信小程序的appid和appSecret
appId: xxx
appSecret: xxx
微信配置类
- 读取并映射,yml中的自定义属性到Java类的属性上
/**
* 微信配置的Properties属性类
*/
@Setter
@Getter
@NoArgsConstructor
@ToString
@Configuration
@ConfigurationProperties(prefix = "zzyl.wechat")
public class WeChatConfigProperties {
/**
* 微信小程序的AppId
*/
private String appId;
/**
* 微信小程序的AppSecret
*/
private String appSecret;
}
WechatService,微信业务层接口
/**
* 微信业务层接口
*/
public interface WechatService {
/**
* 获取openid
*
* @param code 登录凭证
*/
String getOpenid(String code);
/**
* 获取手机号
*
* @param phoneCode 手机号凭证
*/
String getPhoneNumber(String phoneCode);
}
WechatServiceImpl,微信业务层实现类
- 主要有3个接口需要我们调用,分别为:
-
jscode2session
,通过登录临时凭证code
,换取微信小程序的openId
,也就是小程序登录
-
token
,获取微信小程序的AccessToken
,获取用户手机号请求中,需要添加该参数
-
getuserphonenumber
,通过获取手机号临时凭证phoneCode
,获取用户的手机号
/**
* 微信业务层实现类
*/
@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";
/**
* 获取手机号
*/
private static final String PHONE_REQUEST_URL = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=";
/**
* 微信配置信息
*/
@Autowired
private WeChatConfigProperties weChatConfigProperties;
/**
* 获取openid
*
* @param code 微信小程序的登录临时凭证
*/
@Override
public String getOpenid(String code) {
// 获取公共参数
Map<String, Object> requestUrlParam = getAppConfig();
// 登录时获取的 code,可通过wx.login获取
requestUrlParam.put("js_code", code);
// 授权类型
requestUrlParam.put("grant_type", "authorization_code");
// 发起请求,用code换取openId
String result = HttpUtil.get(CODE_2_SESSION_URL, requestUrlParam);
JSONObject jsonObject = JSONUtil.parseObj(result);
// 若code不正确,则获取不到openid,那么响应失败
Integer errCode = jsonObject.getInt("errcode");
if (errCode != null && errCode != 0) {
// 错误信息
String errMsg = jsonObject.getStr("errmsg");
throw new RuntimeException(errMsg);
}
return jsonObject.getStr("openid");
}
/**
* 获取手机号
*
* @param phoneCode 手机号凭证
*/
@Override
public String getPhoneNumber(String phoneCode) {
// 获取微信的AccessToken
String accessToken = getAccessToken();
// 将token,拼接到请求路径中
String url = PHONE_REQUEST_URL + accessToken;
Map<String, Object> param = new HashMap<>();
// 手机号临时凭证
param.put("code", phoneCode);
// 参数要转成json,不能直接传Map,否则会返回失败,提示请求参数格式不正确
String json = JSONUtil.toJsonStr(param);
// 发起请求
String result = HttpUtil.post(url, json);
// 解析响应
JSONObject jsonObject = JSONUtil.parseObj(result);
Integer errCode = jsonObject.getInt("errcode");
// 若code不正确,则获取不到手机号,那么响应失败
if (errCode != 0) {
// 错误信息
String errMsg = jsonObject.getStr("errmsg");
throw new RuntimeException(errMsg);
}
/*
响应示例:
{
"errcode":0,
"errmsg":"ok",
"phone_info": {
"phoneNumber":"xxxxxx",
"purePhoneNumber": "xxxxxx",
"countryCode": 86,
"watermark": {
"timestamp": 1637744274,
"appid": "xxxx"
}
}
}
*/
// 用户的手机号信息
JSONObject phoneInfo = jsonObject.getJSONObject("phone_info");
// 获取手机号
return phoneInfo.getStr("purePhoneNumber");
}
/**
* 获取微信的AccessToken
*/
public String getAccessToken() {
// 获取公共参数
Map<String, Object> requestUrlParam = getAppConfig();
// 授权类型
requestUrlParam.put("grant_type", "client_credential");
// 发起请求
String result = HttpUtil.get(TOKEN_URL, requestUrlParam);
// 解析响应
JSONObject jsonObject = JSONUtil.parseObj(result);
// 错误码
Integer errCode = jsonObject.getInt("errcode");
// 如果错误,则返回错误信息
if (errCode != null && errCode != 0) {
// 错误信息
String errMsg = jsonObject.getStr("errmsg");
throw new RuntimeException(errMsg);
}
return jsonObject.getStr("access_token");
}
/**
* 获取公共参数
*/
private Map<String, Object> getAppConfig() {
Map<String, Object> requestUrlParam = new HashMap<>();
requestUrlParam.put("appid", weChatConfigProperties.getAppId());
requestUrlParam.put("secret", weChatConfigProperties.getAppSecret());
return requestUrlParam;
}
}