微信扫描验证登录
前言
昨天整合了榛子云实现了短信服务验证登录,今天继续实现第二种登录方式,即微信扫码验证登录~
什么是OAuth2
简单来说,OAuth2就是一种授权认证,一种针对开放系统间授权,单点登录和现代微服务安全的解决方案。
开放系统间授权:不同系统应用间可以通过互相访问,而不需要重新进行登录验证,比如用户需要从微信跳转到天猫商城,这时候用户在微信已经登录了,但是在天猫商城中没有登录,没有具体的访问权限,这时候通过OAuth2的令牌访问即可较好地解决此问题
单点登录:一个模块登录,其它模块就不需要进行登录了(即一次登录,处处访问)
注意:OAuth2.0仅是一个授权框架,仅用于授权代理,即不同系统间的授权访问,而这个授权是可以通过令牌实现的,正因为需要考虑如何管理令牌、颁发令牌、吊销令牌,需要统一的协议,因此就有了OAuth2协议
微信扫码验证登录思路及流程
码前流程构想及细节考虑
上面我们简单地了解了一下OAuth2是什么,接下来我们就理清一下如何实现微信的扫码验证登录
我们体验过扫码登录流程,大致流程如下:
待扫描- - ->已扫描,待确认- - ->已确认
- 前端发起一个使用二维码登录的请求,后端接收到这个请求,然后返回一个生成的二维码或生成二维码的信息给前端
- 用户接收到这个二维码,扫码
- 扫码成功,手机端确认登录
- 手机端确认登录,后端接收到这个响应,然后根据用户是否注册进行信息录入,若用户存在,那么就直接响应用户信息给前端,否则进行简单注册后(用户名,头像等),然后返回给前端(基于JWT的token登录验证)
- 前端接收到信息,存储用户的cookie,然后登陆成功
其实其中还有非常多细节需要我们注意:
- 比如需要根据生成的唯一二维码与用户的身份信息进行绑定生成的临时token,否则容易出现不同用户之间的登录错乱的问题
- 需要有确认登录这个步骤,确保用户安全
- 服务端校验临时token并生成正式token,后续用户便可以携带这个正式token访问服务端
当然啦,我这里就没有考虑到细节因素,这部分由于自己申请不下来就使用的是尚硅谷提供的二维码扫描,也就没有唯一绑定
实现步骤
- 使用方法生成微信扫码的二维码
- 返回生成二维码所需要的参数给前端
- 前端拿到参数,然后在前端中调用远程接口获取二维码,页面跳转
- 扫描二维码,手机确认登录
- 手机确认登录后,微信的服务器调用回调地址,跳转到本地方法接口中(回调本地方法),回调时传递code【临时token】和state【状态】,然后在本地方法中
- 第三方应用获取到接口调用凭证,微信服务器校验
- 校验无误,然后可以获取到用户个人信息,根据个人信息进行登录验证即可
图片来源于B站拓薪教育
实现
1.获取二维码参数信息
/** 获取微信登录参数
*
* @param session
* @return
* @throws UnsupportedEncodingException
*
* 1. 该方法生成微信扫描的二维码
* 2. 返回生成二维码所需要的参数
*/
@GetMapping("getLoginParam")
@ResponseBody //通过这个注解可以返回数据
public Result genQrConnect(HttpSession session) throws UnsupportedEncodingException {
Map<String,Object> map = new HashMap<>();
String redirectUri = URLEncoder.encode(ConstantWxPropertiesUtils.WX_OPEN_REDIRECT_URL, "UTF-8");
//必须
map.put("appid", ConstantWxPropertiesUtils.WX_OPEN_APP_ID);
map.put("redirectUri",redirectUri);
map.put("scope", "snsapi_login");
//非必须
map.put("state",System.currentTimeMillis()+"");
return Result.ok(map);
}
2.前端调用接口,生成二维码
weixinLogin() {
this.dialogAtrr.showLoginType = 'weixin'
weixinApi.getLoginParam().then(response => {
var obj = new WxLogin({
self_redirect:true,
id: 'weixinLogin', // 需要显示的容器id
appid: response.data.appid, // 公众号appid wx*******
scope: response.data.scope, // 网页默认即可
redirect_uri: response.data.redirectUri, // 授权成功后回调的url
state: response.data.state, // 可设置为简单的随机数加session用来校验
style: 'black', // 提供"black"、"white"可选。二维码的样式
href: '' // 外部css文件url,需要https
})
})
},
3.1获取临时票据(code)
//1.获取临时票据code[拦截非法回调]
if (StringUtils.isBlank(state) || StringUtils.isBlank(code)) {
log.error("非法回调请求");
throw new YyghException(ResultCodeEnum.ILLEGAL_CALLBACK_REQUEST_ERROR);
}
3.2使用code+appid+secret换取access_token
//2.使用code和appid以及appscrect换取access_token
StringBuffer baseAccessTokenUrl = new StringBuffer()
.append("https://api.weixin.qq.com/sns/oauth2/access_token")
.append("?appid=%s")
.append("&secret=%s")
.append("&code=%s")
.append("&grant_type=authorization_code");
String accessTokenUrl = String.format(baseAccessTokenUrl.toString(),
ConstantWxPropertiesUtils.WX_OPEN_APP_ID,
ConstantWxPropertiesUtils.WX_OPEN_APP_SECRET,
code);
//3.根据HttpClientUtils进行http请求回调
String result = null;
try {
result = HttpClientUtils.get(accessTokenUrl);
} catch (Exception e) {
throw new YyghException(ResultCodeEnum.FETCH_ACCESSTOKEN_FAILD);
}
System.out.println("使用code换取的access_token结果 = " + result);
3.3根据获取的access_token进行提取获得openid(用户信息唯一标识)
JSONObject resultJson = JSONObject.parseObject(result);
if(resultJson.getString("errcode") != null){
log.error("获取access_token失败:" + resultJson.getString("errcode") + resultJson.getString("errmsg"));
throw new YyghException(ResultCodeEnum.FETCH_ACCESSTOKEN_FAILD);
}
String accessToken = resultJson.getString("access_token");
String openId = resultJson.getString("openid");
log.info(accessToken);
log.info(openId);
3.4根据openid进行登录
//判断数据库中是否存在扫码人的信息(根据openid进行唯一标识判断)
UserInfo userInfo = userInfoService.selectWxInfoOpenId(openId);
if(userInfo == null){ //数据库中不存在信息,先对用户信息进行存储
//4.拿着openid和access_token请求微信地址,得到扫描人信息
//具体步骤:
//根据access_token获取微信用户的基本信息
//先根据openid进行数据库查询
// UserInfo userInfo = userInfoService.getByOpenid(openId);
// 如果没有查到用户信息,那么调用微信个人信息获取的接口
// if(null == userInfo){
//如果查询到个人信息,那么直接进行登录
//使用access_token换取受保护的资源:微信的个人信息
String baseUserInfoUrl = "https://api.weixin.qq.com/sns/userinfo" +
"?access_token=%s" +
"&openid=%s";
String userInfoUrl = String.format(baseUserInfoUrl, accessToken, openId);
String resultUserInfo = null;
try {
resultUserInfo = HttpClientUtils.get(userInfoUrl);
} catch (Exception e) {
throw new YyghException(ResultCodeEnum.FETCH_USERINFO_ERROR);
}
System.out.println("使用access_token获取用户信息的结果 = " + resultUserInfo);
JSONObject resultUserInfoJson = JSONObject.parseObject(resultUserInfo);
if(resultUserInfoJson.getString("errcode") != null){
log.error("获取用户信息失败:" + resultUserInfoJson.getString("errcode") + resultUserInfoJson.getString("errmsg"));
throw new YyghException(ResultCodeEnum.FETCH_USERINFO_ERROR);
}
//5.解析用户信息(用户昵称和用户头像)
String nickname = resultUserInfoJson.getString("nickname");
String headimgurl = resultUserInfoJson.getString("headimgurl");
//添加到数据库
userInfo = new UserInfo();
userInfo.setOpenid(openId);
userInfo.setNickName(nickname);
userInfo.setStatus(1);
userInfoService.save(userInfo);
}
//6.将获取到的扫码人的信息添加到数据库
Map<String, Object> map = new HashMap<>();
String name = userInfo.getName();
if(StringUtils.isBlank(name)) {
name = userInfo.getNickName();
}
if(StringUtils.isBlank(name)) {
name = userInfo.getPhone();
}
map.put("name", name);
//判断手机号是否为空,如果手机号为空,那么返回openid,否则返回手机号
if(StringUtils.isBlank(userInfo.getPhone())) {
map.put("openid", userInfo.getOpenid());
} else {
map.put("openid", "");
}
String token = JwtHelper.createToken(userInfo.getId(), name);
map.put("token", token);
3.5 将用户信息返回给前端
return "redirect:"
+ ConstantWxPropertiesUtils.YYGH_BASE_URL
+ "/weixin/callback?token="+map.get("token")
+"&openid="+map.get("openid")
+"&name="+ URLEncoder.encode((String)map.get("name"),"utf-8");
3.6登录验证总代码
@GetMapping("callback")
public String callback(String code,String state) throws UnsupportedEncodingException {
//1.获取临时票据code[拦截非法回调]
if (StringUtils.isBlank(state) || StringUtils.isBlank(code)) {
log.error("非法回调请求");
throw new YyghException(ResultCodeEnum.ILLEGAL_CALLBACK_REQUEST_ERROR);
}
//2.使用code和appid以及appscrect换取access_token
StringBuffer baseAccessTokenUrl = new StringBuffer()
.append("https://api.weixin.qq.com/sns/oauth2/access_token")
.append("?appid=%s")
.append("&secret=%s")
.append("&code=%s")
.append("&grant_type=authorization_code");
String accessTokenUrl = String.format(baseAccessTokenUrl.toString(),
ConstantWxPropertiesUtils.WX_OPEN_APP_ID,
ConstantWxPropertiesUtils.WX_OPEN_APP_SECRET,
code);
//3.根据HttpClientUtils进行http请求回调
String result = null;
try {
result = HttpClientUtils.get(accessTokenUrl);
} catch (Exception e) {
throw new YyghException(ResultCodeEnum.FETCH_ACCESSTOKEN_FAILD);
}
System.out.println("使用code换取的access_token结果 = " + result);
JSONObject resultJson = JSONObject.parseObject(result);
if(resultJson.getString("errcode") != null){
log.error("获取access_token失败:" + resultJson.getString("errcode") + resultJson.getString("errmsg"));
throw new YyghException(ResultCodeEnum.FETCH_ACCESSTOKEN_FAILD);
}
String accessToken = resultJson.getString("access_token");
String openId = resultJson.getString("openid");
log.info(accessToken);
log.info(openId);
//判断数据库中是否存在扫码人的信息(根据openid进行唯一标识判断)
UserInfo userInfo = userInfoService.selectWxInfoOpenId(openId);
if(userInfo == null){ //数据库中不存在信息,先对用户信息进行存储
//4.拿着openid和access_token请求微信地址,得到扫描人信息
//具体步骤:
//根据access_token获取微信用户的基本信息
//先根据openid进行数据库查询
// UserInfo userInfo = userInfoService.getByOpenid(openId);
// 如果没有查到用户信息,那么调用微信个人信息获取的接口
// if(null == userInfo){
//如果查询到个人信息,那么直接进行登录
//使用access_token换取受保护的资源:微信的个人信息
String baseUserInfoUrl = "https://api.weixin.qq.com/sns/userinfo" +
"?access_token=%s" +
"&openid=%s";
String userInfoUrl = String.format(baseUserInfoUrl, accessToken, openId);
String resultUserInfo = null;
try {
resultUserInfo = HttpClientUtils.get(userInfoUrl);
} catch (Exception e) {
throw new YyghException(ResultCodeEnum.FETCH_USERINFO_ERROR);
}
System.out.println("使用access_token获取用户信息的结果 = " + resultUserInfo);
JSONObject resultUserInfoJson = JSONObject.parseObject(resultUserInfo);
if(resultUserInfoJson.getString("errcode") != null){
log.error("获取用户信息失败:" + resultUserInfoJson.getString("errcode") + resultUserInfoJson.getString("errmsg"));
throw new YyghException(ResultCodeEnum.FETCH_USERINFO_ERROR);
}
//5.解析用户信息(用户昵称和用户头像)
String nickname = resultUserInfoJson.getString("nickname");
String headimgurl = resultUserInfoJson.getString("headimgurl");
//添加到数据库
userInfo = new UserInfo();
userInfo.setOpenid(openId);
userInfo.setNickName(nickname);
userInfo.setStatus(1);
userInfoService.save(userInfo);
}
//6.将获取到的扫码人的信息添加到数据库
Map<String, Object> map = new HashMap<>();
String name = userInfo.getName();
if(StringUtils.isBlank(name)) {
name = userInfo.getNickName();
}
if(StringUtils.isBlank(name)) {
name = userInfo.getPhone();
}
map.put("name", name);
//判断手机号是否为空,如果手机号为空,那么返回openid,否则返回手机号
if(StringUtils.isBlank(userInfo.getPhone())) {
map.put("openid", userInfo.getOpenid());
} else {
map.put("openid", "");
}
String token = JwtHelper.createToken(userInfo.getId(), name);
map.put("token", token);
return "redirect:"
+ ConstantWxPropertiesUtils.YYGH_BASE_URL
+ "/weixin/callback?token="+map.get("token")
+"&openid="+map.get("openid")
+"&name="+ URLEncoder.encode((String)map.get("name"),"utf-8");
}
4. 前端接收参数并作校验登录
callback.vue
<template>
<!-- header -->
<div>
</div>
<!-- footer -->
</template>
<script>
export default {
layout: "empty",
data() {
return {
}
},
mounted() {
let token = this.$route.query.token
let name = this.$route.query.name
let openid = this.$route.query.openid
// 调用父vue方法
window.parent['loginCallback'](name, token, openid)
}
}
</script>
myheader.vue
loginCallback(name, token, openid) {
// 打开手机登录层,绑定手机号,改逻辑与手机登录一致
if(openid != '') {
this.userInfo.openid = openid
this.showLogin()
} else {
this.setCookies(name, token)
}
},
总结
将微信登录流程搞清楚才能在代码实战中清楚,这方面还有待加强~