目录
前期准备
登录微信公众平台
https://mp.weixin.qq.com/
用此功能可能需要首页一些列认证完成后才可以:eg:小程序备案、微信认证等
1、获取AppId和AppSecret
侧开发管理->开发设置->需要获取 AppID(小程序ID)、**AppSecret(小程序密钥) **。
注意:AppSecret第一次生成后需要自己复制保存,后续虽然可以重置但是重置会相对麻烦。
2、更新隐私协议
左侧最底部设置(或点击右上角头像)->基本设置->服务内容声明->用户隐私保护指引 需要更新隐私协议否则后面会影响小程序按钮呼叫不出选择手机号的弹窗。隐私内容根据个人小程序用途如实填写即可。
3、查看手机号验证额度
注意:如果按照开头所说,完成小程序备案、微信认证等点击右侧付费管理-》用量统计-》手机号快速验证组件,有标注资源包总量(如下图资源包有1000次体验次数后续可自行加购)。
4、在uni-app和微信开发者工具中填写自己第一步获取到的AppId
1、uni-app中
2、微信开发者工具中
前端uniapp代码
1、添加获取手机号按钮
因为我们这边需要调用的是手机号所以open-type=getPhoneNumber和@getphonenumber=‘自定义方法’。
@getphonenumber是用户在弹出申请获取用户手机号时用户手动点击了手机号的回调。
<button class="loginPopup-btnList-login" open-type="getPhoneNumber" @getphonenumber="wxPhonelogin">
微信手机号登录
</button>
2、自定义方法实现
实现思路:
1、先用uni.login返回的用户登录凭证,去后端解析出open_id,如果有存在相应的open_id,则直接返回用户执行登录。
2、如果用户第一次手机号授权登录系统,将微信open_id存入数据库用户表中,返回用户执行登录。
参考uni.login参考文档:uni.login API
async wxPhonelogin(e) {
const _this = this
// 开启遮罩层
_this.showLoadings(true);
// 调用uni-app提供登录接口
uni.login({
const _this = this
// 用户点击了允许后操作
if (e.detail.errMsg == 'getPhoneNumber:ok') {
// 开启遮罩层
_this.showLoadings(true);
uni.login({
// 登录服务提供商,我们需要微信登录
provider: "weixin",
async success(loginRes) {
// 调用uni-app提供登录接口
let data = {
// 用户登录凭证。开发者需要在开发者服务器后台,使用 code 换取 openid 和 session_key 等信息
code: loginRes.code,
// 微信授权手机弹窗选择手机后返回code,使用access_token可以解析出手机号
mobileCode: e.detail.code,
}
try {
// 如果用户表已经存在code对应解析的openId,直接完成登录
const userOpenIdRes = await openIdLoginApi(data)
if (userOpenIdRes.success && userOpenIdRes.data !== null) {
_this.tellPhoneValue = userOpenIdRes.data.account
// 获取返回数据 执行操作
_this.loginAfter(userOpenIdRes, 'wxLogin');
_this.getUserInfo();
}
// 关闭遮罩层
_this.showLoadings(false);
} catch (e) {
// 关闭遮罩层
_this.showLoadings(false);
// 提示错误信息
if (e.data.msg) {
_this.setShowToastFunc(e.data.msg);
} else {
_this.setShowToastFunc('登录异常~');
}
return;
}
}
})
} else {
// 用户点解拒绝后不做操作
return;
}
},
3、对应请求接口
import {requestApi} from './requestApi.js';
const url = '/thirdPartyLogin';
// 微信openID登录
function openIdLoginApi(data) {
return requestApi({
url: url + '/openIdLogin',
data: data,
method: 'POST'
});
}
export {
wxLogin,
openIdLoginApi
}
4、当前效果
正常到这一步,应该点击按钮就会弹出选择手机号
如果未出弹出选择手机号,需要查看控制台报错信息。可能是前期工作未处理好的原因!!
如下效果
微信开发者工具和华为真机效果。
后端java代码
1、controller
@Api(value = "第三方登录", tags = "第三方登录")
@RestController
@RequestMapping("thirdPartyLogin")
@AllArgsConstructor
public class ThirdPartyLoginController {
private ThirdPartyLoginService thirdPartyLoginService;
@PostMapping("/openIdLogin")
@ApiOperationSupport(order = 1)
@ApiOperation(value = "微信OpenId登录", notes = "微信OpenId登录")
public R<Object> openIdLogin(@RequestBody ThirdPartyWxLoginDto params) {
AuthInfo authInfo = thirdPartyLoginService.openIdLogin(params);
Map<String, String> map = new HashMap<>(6);
map.put("token", authInfo.getAccessToken());
map.put("userId", authInfo.getUserId().toString());
map.put("userName", authInfo.getUserName());
map.put("account", authInfo.getAccount());
// 手机号脱敏
String s = authInfo.getPhone().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2");
map.put("userPhone",s);
return R.data(map);
}
}
2、service
public interface ThirdPartyLoginService {
AuthInfo openIdLogin(ThirdPartyWxLoginDto params);
}
3、实现类
代码中用到了restTemplate做请求,需要引入相关依赖和配置。请自行百度。
用到的API
(1)用户手机号验证 :手机号快速验证
(2)第一步用户手机号验证调用需要带参ACCESS_TOKEN:获取接口调用凭据
(3)根据用户uni.login获取到的登录凭证code解析出open_id :小程序登录
@Service
@Slf4j
public class ThirdPartyLoginServiceImpl implements ThirdPartyLoginService {
@Autowired
private RedisUtil redisUtil;
@Autowired
private RestTemplate restTemplate;
@Autowired
private IUserService iUserService;
@Autowired
private UserMapper userMapper;
@Autowired
private IDhMessagePushService iDhMessagePushService;
/**获取手机号*/
public static final String WX_GET_PHONE_NUMBER_URL = "https://api.weixin.qq.com/wxa/business/getuserphonenumber";
/**获取接口调用凭据,需要此凭据解析手机号*/
public static final String WX_ACCESE_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token";
/*** 获取open_id接口*/
public static final String OPEN_ID_URL = "https://api.weixin.qq.com/sns/jscode2session";
@Value("${wx.config.appSecret}")
private String appSecret;
@Value("${wx.config.appId}")
private String appId;
public static final String REDIS_KEY_ACCESS_TOKEN = "REDIS_KEY_ACCESS_TOKEN";
/**
* 微信openID验证登录,如果用户表有openId直接验证登录,否则走微信手机号验证登录
*/
@Override
public AuthInfo openIdLogin(ThirdPartyWxLoginDto params) {
// 获取用户openId,判断数据库是否有存入该openId了,如果有代表之前登录过了就不用走手机验证
HashMap<String, String> userOpenId = getUserOpenId(params);
String openid = userOpenId.get("openid");
QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
userQueryWrapper.lambda().eq(User::getOpenId,openid);
List<User> list = iUserService.list(userQueryWrapper);
if (CollectionUtil.isNotEmpty(list)) {
if (list.size() > 1) {
// 如果有多个则微信授权找到手机号进行登录
return wxLogin(params.getMobileCode(),openid);
} else {
// 如果只有一个则判断 用户是否失效,若正常则返回
User user = list.get(0);
if (Objects.nonNull(user.getFailureTime())) {
throw new RuntimeException("当前手机号已失效无权限登录~");
}
UserInfo userInfo = getUserInfo(user);
return TokenUtil.createAuthInfo(userInfo);
}
}
// 如果为空则微信授权登录
return wxLogin(params.getMobileCode(),openid);
}
/**
* 微信手机号验证登录
*/
private AuthInfo wxLogin(String mobileCode , String openid) {
log.info("=== 微信手机授权登录开始 ===");
String purePhoneNumber = getWxUserPhone(mobileCode);
// 根据手机号查询用户表 判断是否有该手机号如果没有的话则 不允许登录
QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
userQueryWrapper.lambda().eq(User::getAccount,purePhoneNumber);
User user = iUserService.getOne(userQueryWrapper);
if (Objects.isNull(user) || Objects.nonNull(user.getFailureTime())) {
throw new RuntimeException("当前手机号无系统权限或者已失效~");
}
user.setOpenId(openid);
user.setPhone(purePhoneNumber);
iUserService.saveOrUpdate(user);
// 封装返回数据
UserInfo userInfo = getUserInfo(user);
return TokenUtil.createAuthInfo(userInfo);
}
/**
* 解析用户手机号
*/
private String getWxUserPhone(String mobileCode) {
// 获取Access_Token
String accessToken = getAccessToken();
// 请求解析手机号
StringBuilder getPhoneNumberUrl = new StringBuilder(WX_GET_PHONE_NUMBER_URL);
getPhoneNumberUrl.append("?access_token=").append(accessToken);
Map<String, String> phoneNumberParam = new HashMap<>();
phoneNumberParam.put("code", mobileCode);
ResponseEntity<HashMap> hashMapResponseEntity = restTemplate.postForEntity(getPhoneNumberUrl.toString(), phoneNumberParam, HashMap.class);
if (hashMapResponseEntity.getStatusCodeValue() != 200 || Objects.isNull(hashMapResponseEntity.getBody()) || (Integer) hashMapResponseEntity.getBody().get("errcode")!=0) {
throw new RuntimeException("解析手机接口调用异常~");
}
log.info("=== 微信手机授权请求解析手机号返回:{} ",hashMapResponseEntity.getBody());
LinkedHashMap phoneInfo = (LinkedHashMap)hashMapResponseEntity.getBody().get("phone_info");
String purePhoneNumber = (String)phoneInfo.get("purePhoneNumber");
return purePhoneNumber;
}
/**
* 获取Access_Token,消息订阅或者微信授权都需要这个东西,可以放入缓存因为每次获取都有两个小时有效期
*/
private String getAccessToken() {
// 查询redis 是否有存放access_token避免过多次请求
Object redisRes = redisUtil.get(REDIS_KEY_ACCESS_TOKEN);
if (Objects.nonNull(redisRes)) {
return redisRes.toString();
}
// 请求ACCESS_TOKEN
StringBuilder accessTokenUrl = new StringBuilder(WX_ACCESE_TOKEN_URL);
accessTokenUrl.append("?grant_type=client_credential");
accessTokenUrl.append("&appid=").append(appId);
accessTokenUrl.append("&secret=").append(appSecret);
ResponseEntity<HashMap> accessTokenRes = restTemplate.getForEntity(accessTokenUrl.toString(), HashMap.class);
if (accessTokenRes.getStatusCodeValue() != 200 || Objects.isNull(accessTokenRes.getBody()) || Objects.nonNull(accessTokenRes.getBody().get("errcode"))) {
throw new RuntimeException("Token接口调用异常~");
}
log.info("=== 微信手机授权请求AccessToken返回:{} ",accessTokenRes.getBody());
String accessToken = accessTokenRes.getBody().get("access_token").toString();
// 存放redis 设置两个小时超时时间
redisUtil.set(REDIS_KEY_ACCESS_TOKEN,accessToken,2L, TimeUnit.HOURS);
return accessToken;
}
/**
* 获取用户OpenId
*/
private HashMap<String, String> getUserOpenId(ThirdPartyWxLoginDto params) {
StringBuilder openIdUrl = new StringBuilder(OPEN_ID_URL);
openIdUrl.append("?grant_type=").append("authorization_code");
openIdUrl.append("&appid=").append(appId);
openIdUrl.append("&secret=").append(appSecret);
openIdUrl.append("&js_code=").append(params.getCode());
ResponseEntity<String> openIdRes = restTemplate.getForEntity(openIdUrl.toString(), String.class);
if (openIdRes.getStatusCodeValue() != 200 || Objects.isNull(openIdRes.getBody())) {
throw new RuntimeException("OpenId接口调用异常~");
}
HashMap<String,String> parse = JSON.parseObject(openIdRes.getBody(),HashMap.class);
if (Objects.nonNull(parse.get("errcode"))) {
throw new RuntimeException("OpenId接口调用异常~");
}
return parse;
}
}
4、实体类
@Data
public class ThirdPartyWxLoginDto {
/**
* 用户uni.login登录凭证code
* */
private String code;
/**
* 手机号Code
* */
private String mobileCode;
}