一、实现微信登录在编码前需要在微信开放平台注册开发者账号和安装NatApp获取域名。
微信开放平台注册开发者账号可参考:https://blog.csdn.net/weixin_45001200/article/details/119495080?spm=1001.2014.3001.5502
安装NatApp获取域名可参考:https://blog.csdn.net/weixin_45001200/article/details/119495198?spm=1001.2014.3001.5502
二、微信登录步骤:
1、点击登录页面的微信注册登录
2、页面跳转到微信登录的页面,后台以appid和appsecret为参数调用微信api获取access_token
3、以access_token为参数调用微信api获取本地微信登录二维码ticket,同时传随机字符串scene_str,该参与可用于程序验证是否扫码成功
4、通过ticket获取二维码成功,页面显示,同时前台开始进行js定时任务,监听是否扫码成功
5、用户微信扫码成功,如果是第一次微信扫码,则弹出微信注册的框用户绑定手机号和密码进行注册
6、微信服务器端回调本地服务器签名验证接口验证签名和回调处理
7、回调处理确定扫码成功,缓存插入本地扫码成功记录
8、前台定时任务获取到扫码成功记录或者后台主动通知,进行扫码成功后业务处理。
三、前端Vue代码实现
登录页面的代码:
//login.vue中的代码
<template>
<router-link type="info" to='/loginWX'>微信注册登录</router-link>
</template>
登录页面的效果如下的红色框:
微信登录页面的代码:
//loginWX.vue中的代码
<template>
<el-form class="div1">
<p>请扫描二维码</p>
<img v-bind:src="qrCodeImgUrl" v-show="qrCodeImgFlag" class="div3">
</el-form>
<el-form class="div2" v-show="registerFlag">
<p class="div02">微信绑定</p>
<el-input class="div12" type="text" v-model="phone" placeholder="手机号"></el-input>
<el-input class="div22" type="password" v-model="password" placeholder="密码"></el-input>
<el-button class="div32" type="primary" style="width:48%;" @click.native.prevent="register" >提 交</el-button>
</el-form>
</template>
1)mounted()方法中调用了this.getQrCode(),在页面一加载进来的时候,就自动执行this.getQrCode()获取二维码的方法,二维码生成成功后,调用扫描二维码的方法。
2)微信扫描二维码的方法getOpenId1()设置了一个定时器,未扫码的时候getOpenId1()一直调用,当扫码完成之后定时器清除,getOpenId1()方法停止被调用。
3)微信扫描二维码即微信登录,扫描二维码获取用户微信的openId,在后台进行判断,如果没有该用户的openId,则弹出微信绑定的框,进行微信绑定,绑定成功后,直接跳到系统的主页面完成登录。如果存在该用户的openId,则直接跳到系统的主页面完成登录。
<script>
export default {
name: "LoginWX",
data(){
return{
qrCodeImgUrl: '',
qrCodeImgFlag: false,
registerFlag: false,
timer: '',
phone: '',
password: '',
openId1: ''
}
},
methods: {
//微信扫描二维码
getOpenId1: function (x) {
this.timer = setInterval(() => {
let userInfo = {eventKey: x.sceneStr}
this.$api.login.getOpenId(JSON.stringify(userInfo)).then((res) => {
if (res.code == 200) {
clearInterval(this.timer)
let userInfo1 = {openId: res.data.openId}
this.$api.login.wXLogin(JSON.stringify(userInfo1)).then((res) => {
if (res.code == 200) {
console.log("微信登录成功!");
sessionStorage.setItem('userInfo', JSON.stringify(res.data)) // 保存用户到本地会话
sessionStorage.setItem('token', res.data.token) // 保存用户到本地会话
this.$router.push('/SelectRole')
}else if(res.code==300){
//注册微信
this.registerFlag = true;
this.register(res.data);
}
}).catch(function (res) {
//alert(res);
//this.$message({message: '微信登录失败, ' + res.msg, type: 'error'})
});
}
}).catch(function (res) {
//alert(res);
});
}, 3000)
},
// 获取登录二维码
getQrCode: function () {
this.$api.login.getQrCode().then((res) => {
console.log("getQrCode的res.data: " + res.data);
if (res.code == 200) {
console.log("生成二维码成功!")
this.qrCodeImgUrl = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=" + res.data.ticket;
this.qrCodeImgFlag = true;
this.getOpenId1(res.data);
} else {
// console.log("生成二维码失败!");
}
}).catch(function (res) {
alert(res);
});
},
//微信绑定
register: function(x){
if(x.openId!=null){
this.openId1 = x.openId
}
if(this.phone!=null&&this.password!=null&&this.openId1!=null){
clearInterval(this.timer)
let userInfo = {phone: this.phone,password:this.password,openId:this.openId1}
this.$api.login.wxRegister(JSON.stringify(userInfo)).then((res) => {
if (res.code == 200) {
this.$message({message: '微信注册成功! '})
this.$router.push('/SelectRole')
} else {
// this.$message({message: '微信注册失败! '})
}
}).catch(function (res) {
alert(res);
});
}
}
},
mounted()
{
console.log('come in loginWX vue.....');
this.getQrCode()
}
}
</script>
微信登录页面效果如下:
四、spring boot后端代码实现
LoginController层次实现了微信登录和微信注册功能
/**
*
* @author ztt
* @param sysUserNewVo
* @return com.qcby.device.manage.util.WXResultJson
* @description: 入参:openId phone password
*/
@RequestMapping("wxRegister")
public WXResultJson wxRegister(@RequestBody SysUserNewVo sysUserNewVo){
log.info("wXRegister入参openId:"+sysUserNewVo.getOpenId());
log.info("wXRegister入参phone+password:"+sysUserNewVo.getPhone()+";"+sysUserNewVo.getPassword());
Boolean x = loginService.wxRegister(sysUserNewVo);
if(x){
return WXResultJson.ok("微信注册成功!");
}
return WXResultJson.error("微信注册失败!");
}
/**
* @author ztt
* @param
* @return com.qcby.teaching.msgmanage.common.web.ResultJson
* @description: 微信扫码登录, 入参openId
*/
@RequestMapping("wXLogin")
public WXResultJson wXLogin(@RequestBody WxPcLoginKey wxPcLoginKey){
String openId = wxPcLoginKey.getOpenId();
log.info("wXLogin入参openId:"+openId);
SysUserNewVo sysUserNewVo = loginService.getUserInfoByOpenId(openId);
if(sysUserNewVo==null){
SysUserNewVo sysUserNewVo1 = new SysUserNewVo();
sysUserNewVo1.setOpenId(openId);
return WXResultJson.error("请注册微信!",sysUserNewVo1);
}
log.info("微信登录成功!");
String token = JwtUtil.createToken(sysUserNewVo.getAccount());
sysUserNewVo.setToken(token);
List<SysRole> roleList = sysRoleService.getRoleById(sysUserNewVo.getId());
sysUserNewVo.setRoleList(roleList);
redisUtil.set("SysUser",JSON.toJSONString(sysUserNewVo));
SysUserNewVo xxx = JSON.parseObject(String.valueOf(redisUtil.get("SysUser")), SysUserNewVo.class);
redisUtil.set("sysUserAccount",sysUserNewVo.getAccount());
return WXResultJson.ok(sysUserNewVo);
}
RestWxPcLoginController层实现了获取登录二维码、 获取accessToken、验证签名、回调方法和获取用户openId方法。
这里需要注意的一个点是:一个系统有多个用户进行微信登录,而eventKey作为每个用户是否扫码成功的验证,因此为了区分不同的用户登录,需要将其key值即eventKey以及对应用户的openId即value值存到map中。
@Slf4j
@RestController
@RequestMapping(GlobalConstant.REST_URL_PREFIX +"/qrCodeFirstLogin")
public class RestWxPcLoginController {
@Autowired
private WxConfig wxConfig;
@Autowired
private LoginService loginService;
// 模拟数据库存储或者缓存存储
Map<String, WxPcLoginKey> loginMap = new ConcurrentHashMap<>(64);
/**
* 获取登录二维码
* @return
*/
@GetMapping("/getQrCode")
private WXResultJson getQrCode(){
log.info("getQrCode入参!");
try {
// 获取token开发者
String accessToken =getAccessToken();
String getQrCodeUrl = wxConfig.getQrCodeUrl().replace("TOKEN", accessToken);
// 这里生成一个带参数的二维码,参数是scene_str
String sceneStr = CodeLoginUtil.getRandomString(8);
String json="{\"expire_seconds\": 604800, \"action_name\": \"QR_STR_SCENE\"" +", \"action_info\": {\"scene\": {\"scene_str\": \""+sceneStr+"\"}}}";
String result = HttpClientUtil.doPostJson(getQrCodeUrl,json);
JSONObject jsonObject = JSONObject.parseObject(result);
jsonObject.put("sceneStr",sceneStr);
log.info("getQrCode出参成功: "+jsonObject);
return WXResultJson.ok(jsonObject);
} catch (Exception e) {
e.printStackTrace();
log.info("getQrCode出参失败!");
return WXResultJson.error(e.getMessage());
}
}
/**
* 获取accessToken
* @return
*/
public String getAccessToken(){
log.info("getAccessToken入参!");
String accessToken = null;
String getTokenUrl = wxConfig.getTokenUrl().replace("APPID", wxConfig.getAppId()).replace("SECRET", wxConfig.getAppSecret());
String result = HttpClientUtil.doGet(getTokenUrl);
JSONObject jsonObject = JSONObject.parseObject(result);
accessToken = jsonObject.getString("access_token");
log.info("getAccessToken出参成功:"+accessToken);
return accessToken ;
}
/**
* 验证签名
* @param request
* @return
* @throws Exception
*/
@RequestMapping("/checkSign")
public String checkSign ( HttpServletRequest request) throws Exception {
log.info("checkSign入参!");
//获取微信请求参数
String signature = request.getParameter ("signature");
String timestamp = request.getParameter ("timestamp");
String nonce = request.getParameter ("nonce");
String echostr = request.getParameter ("echostr");
//参数排序。 token 就要换成自己实际写的 token
String [] params = new String [] {timestamp,nonce,"123456"} ;
Arrays.sort (params) ;
//拼接
String paramstr = params[0] + params[1] + params[2] ;
//加密
//获取 shal 算法封装类
MessageDigest Sha1Dtgest = MessageDigest.getInstance("SHA-1") ;
//进行加密
byte [] digestResult = Sha1Dtgest.digest(paramstr.getBytes ("UTF-8"));
//拿到加密结果
String mysignature = CodeLoginUtil.bytes2HexString(digestResult);
mysignature=mysignature.toLowerCase(Locale.ROOT);
//是否正确
boolean signsuccess = mysignature.equals(signature);
//逻辑处理
if (signsuccess && echostr!=null) {
//peizhi token
log.info("checkSign出参失败!");
return echostr ;//不正确就直接返回失败提示.
}else{
JSONObject jsonObject = callback(request);
log.info("checkSign出参成功:"+jsonObject.toJSONString());
return jsonObject.toJSONString();
}
}
/**
* 回调方法
* @param request
* @return
* @throws Exception
*/
public JSONObject callback(HttpServletRequest request) throws Exception{
log.info("callback入参!");
//request中有相应的信息,进行解析
WxMpXmlMessage message= WxMpXmlMessage.fromXml(request.getInputStream());//获取消息流,并解析xml
String messageType=message.getMsgType(); //消息类型
String messageEvent=message.getEvent(); //消息事件
String fromUser=message.getFromUser();
//String openid = fromUser;//发送者帐号
String touser=message.getToUser(); //开发者微信号
String text=message.getContent(); //文本消息 文本内容
// 生成二维码时穿过的特殊参数
String eventKey=message.getEventKey(); //二维码参数
log.info("回调函数中的eventKey: "+eventKey+"; fromUser:"+fromUser);
String uuid=""; //从二维码参数中获取uuid通过该uuid可通过websocket前端传数据
String userid="";
//if判断,判断查询
JSONObject jsonObject = new JSONObject();
jsonObject.put("code","200");
if(messageType.equals("event")){
jsonObject = null;
if(messageEvent.equals("SCAN")){
//扫描二维码
//return "欢迎回来";
log.info("callback出参失败,欢迎回来!");
}
if(messageEvent.equals("subscribe")){
//关注
//return "谢谢您的关注";
log.info("callback出参失败,谢谢您的关注!");
}
//没有该用户
if(jsonObject==null){
//从微信上中拉取用户信息
String url = "https://api.weixin.qq.com/cgi-bin/user/info?access_token=" +getAccessToken() +
"&openid=" + fromUser +
"&lang=zh_CN";
String result = HttpClientUtil.doGet(url);
jsonObject = JSONObject.parseObject(result);
/**
* 用户信息处理....
*/
log.info("callback出参失败,没有该用户!");
}
loginMap.put(eventKey,new WxPcLoginKey(eventKey,fromUser));
return jsonObject;
}
log.info("callback出参成功:"+jsonObject);
return jsonObject;
//log.info("消息类型:{},消息事件:{},发送者账号:{},接收者微信:{},文本消息:{},二维码参数:{}",messageType,messageEvent,fromUser,touser,text,eventKey);
}
/**
* 根据二维码标识获取用户openId=>获取用户信息
* @param
* @return
*/
@RequestMapping("getOpenId")
public WXResultJson getOpenId(@RequestBody WxPcLoginKey wxPcLoginKey) throws IOException {
String eventKey = wxPcLoginKey.getEventKey();
eventKey = eventKey.replace("\"","");
if(loginMap.get(eventKey) == null){
return WXResultJson.error("未扫码成功!") ;
}
WxPcLoginKey wxPCLoginKey = loginMap.get(eventKey);
String openId = wxPCLoginKey.getOpenId();
log.info("openId: "+openId);
if(loginMap.get(eventKey) == null){
return WXResultJson.error("未扫码成功!") ;
}
loginMap.remove(eventKey);
log.info("扫码成功!");
WxPcLoginKey wxPcLoginKey1 = new WxPcLoginKey(null,null);
wxPcLoginKey1.setEventKey(eventKey);
wxPcLoginKey1.setOpenId(openId);
return WXResultJson.ok(wxPcLoginKey1);
}
}
yml中的配置如下,natapp中配制了端口为80,所以后端端口需要改成80
后端yml配置文件中关于微信的配置内容如下,后四项是固定不变值,统一使用的。
wx:
appId: wx159ec212eca19da2
appSecret: d0342aacf5c3a4494e7019d1e2cee123
server: http://unhhri.natappfree.cc
qrCodeUrl: https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=TOKEN
tokenUrl: https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=SECRET
openIdUrl: https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=SECRET
userInfoUrl: https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN
server:
port: 80
注:适用本地域名的不好之处就是natApp.exe中生成的域名经常改变,如果想测试微信登录的话,不要忘记将yml中的域名和微信公众台中的域名换成natApp.exe中最新的。
如果项目上线需要买一个域名,将yml中的域名和微信公众台中的域名换掉即可。