上一篇介绍了PC扫码登录通过微信接口获取不限制的小程序码,此篇介绍剩下的内容
element-ui框架下通过SockJS、stompjs实现PC端扫码登录跳转页面(一)
2、已扫描待确认阶段
流程图中第 6 ~ 10 阶段,我们在 PC 端登录微信时,手机扫码后,PC 端的二维码会变成已扫码,请在手机端确认。这个阶段是移动端跟服务端交互的过程。
具体代码如下:
获取小程序码参数有如下一条
params.put("page", "pages/scanLogin/scanLogin");
扫码后进入小程序的scanLogin页面。
<!--scanLogin.wxml-->
<view class="container">
<block>
<image class="logo-image" src="../../static/images/PCAuth.png" mode="widthFix"></image>
<view class="welcome">
欢迎来到
</view>
<view class="welcome-1">
测试评价平台
</view>
<view class="welcome-2">
请确认登录
</view>
<view>
<button class="login-button" bindtap="getUserProfile">
<text>登录</text>
</button>
</view>
</block>
</view>
// scanLogin.js
// 获取应用实例
const app = getApp();
const loginjs = require('../../common/js/login.js')
Page({
data: {
userInfo: {},
hasuserpermistion: false,
scene: ''
},
onShow() {
//小程序隐藏左上角首页按钮
wx.hideHomeButton();
},
onLoad(query) {
//扫码跳转页面是携带scene参数的
const scene = decodeURIComponent(query.scene);
this.setData({
scene: scene,
});
app.globalData.scene = scene;
},
//点击登录按钮
getUserProfile: function(e) {
var self = app;
var that = this;
this.setData({
loadModal: true
})
//首先校验一下本地缓存sessionKey是否过期——设置48小时
var expiredTime = wx.getStorageSync('EXPIREDTIME');
var now = +new Date();
if (expiredTime && now - expiredTime <= 2 * 24 * 60 * 60 * 1000) {
//openid是小程序用户授权后获取的唯一标识
self.globalData.openid = wx.getStorageSync('openid');
var openid = wx.getStorageSync('openid');
var transformdata = new Object({
scene: this.data.scene,
openid: openid
});
self._post_form('/base/shopxo/user/jumpLink', transformdata, function(res) {
if (res.success) {
var userType = res.result.userType;
var token = res.result.token;
//刷新一下缓存有效期
var expiredTime = +new Date() + 2 * 24 * 60 * 60 * 1000;
wx.setStorageSync('EXPIREDTIME', expiredTime);
if (userType == 'custom') {
wx.redirectTo({
url: '/pages/webview/webview?token=' + token
});
} else {
wx.reLaunch({
url: '/pages/welcome/welcome'
});
}
} else {
wx.showModal({
content: res.message,
showCancel: false,
})
}
}, null, function(res) {
that.setData({
loadModal: false
})
}, null)
} else {
//本地缓存过期后需要重新授权登录
wx.getUserProfile({
desc: '微信授权登录', // 声明获取用户个人信息后的用途,后续会展示在弹窗中,请谨慎填写
success: (res) => {
/* console.log("==获取用户消息==", res) */
that.setData({
userInfo: res.userInfo,
hasuserpermistion: true
})
wx.setStorageSync('userInfo', res.userInfo)
self.globalData.hasuserpermistion = true;
self.globalData.userInfo = res.userInfo;
//过期后清除缓存信息
wx.removeStorageSync('sessionKey');
wx.removeStorageSync('openid');
loginjs.PCScanLogin().then(res => {
self.globalData.sessionKey = res.result.session_key;
wx.setStorageSync('openid', res.result.openid);
wx.setStorageSync('sessionKey', res.result.session_key);
if (res.result.state == 'binding') {
wx.navigateTo({
url: '/pages/binding/binding?openid=' + res.result.openid
});
}
if (res.result.state == 'login') {
var userType = res.result.userType;
var token = res.result.token;
//openid有效期2天
var expiredTime = +new Date() + 2 * 24 * 60 * 60 * 1000;
wx.setStorageSync('EXPIREDTIME', expiredTime);
if (userType == 'custom') {
wx.redirectTo({
url: '/pages/webview/webview?token=' + token
});
} else {
wx.reLaunch({
url: '/pages/welcome/welcome'
});
}
}
});
},
fail: err => {
console.log('wx.getUserProfile==>err', err)
},
complete: complete => {
/* console.log('wx.getUserProfile==>complete', complete) */
that.setData({
loadModal: false
})
},
})
}
},
});
小程序本地缓存失效后,需要一个授权登录微信的操作
const loginjs = require('../../common/js/login.js')
wx.getUserProfile({
loginjs.PCScanLogin().then(res => {})
})
common/js/login.js中的方法↓↓↓↓↓
function login(callback) {
var lapp = getApp();
return new Promise(function(resolve, reject) {
if (!lapp.globalData.hasuserpermistion) {
reject(new Error("未授权!"));
return false;
}
wx.login({
complete: (res) => {
//发起网络请求star
var ssself = lapp;
var transformdata = new Object({
JSCODE: res.code,
nickName: lapp.globalData.userInfo.nickName
});
ssself._post_form('/base/shopxo/user/miniProgramLogin', transformdata, function(json) {
/* console.log("wx.login()----", json) */
if (json.success) {
resolve({
result: json.result
});
} else {
wx.showModal({
content: json.message,
showCancel: false,
});
/* reject(new Error(json.message)); */
}
}, null, null);
},
})
})
}
function PCScanLogin(callback) {
var lapp = getApp();
return new Promise(function(resolve, reject) {
if (!lapp.globalData.hasuserpermistion) {
reject(new Error("未授权!"));
return false;
}
wx.login({
complete: (res) => {
//发起网络请求star
var ssself = lapp;
var transformdata = new Object({
JSCODE: res.code,
nickName: lapp.globalData.userInfo.nickName,
scene: lapp.globalData.scene
});
ssself._post_form('/base/shopxo/user/PCScanLogin', transformdata, function(res) {
/* console.log("wx.login()----", res) */
if (res.success) {
resolve({
result: res.result
});
} else {
wx.showModal({
content: res.message,
showCancel: false,
})
}
}, null, null);
},
})
})
}
module.exports = {
login: login,
PCScanLogin: PCScanLogin
};
3、已确认
流程图中的 第 11 ~ 15 步骤,这是扫码登录的最后阶段,移动端携带上一步骤中获取的唯一标识scene ,确认登录,服务端校对完成后,通过springboot webSocket SimpMessagingTemplate向前端发送消息登录成功,跳转页面,并且PC 端生成一个正式的 token ,后续 PC 端就是持有这个 token 访问服务端。
后端代码如下:
小程序本地缓存未失效情况下,走jumpLink这个方法
self._post_form('/base/shopxo/user/jumpLink', transformdata, function(res)
@Autowired
private SimpMessagingTemplate simpMessagingTemplate;
/**
* 扫码授权后缓存openid---PC端跳转页面
*/
@PostMapping(value = "/jumpLink")
public Result<?> jumpLink(@RequestBody JSONObject jsonObject) {
try {
JSONObject result = new JSONObject();
String openId = jsonObject.getString("openid");
String scene = jsonObject.getString("scene");
Object sceneObj = redisUtil.get(scene);
if (sceneObj == null) {
return Result.error("小程序码已经失效,请重新加载二维码");
}
LambdaQueryWrapper<MiniProgramUser> lambdaQuery = new LambdaQueryWrapper<>();
lambdaQuery.eq(MiniProgramUser::getOpenId, openId);
MiniProgramUser one = miniProgramUserService.getOne(lambdaQuery);
if (one == null) {
return Result.error("扫码登录失败,微信未绑定账号");
}
//授权登录成功获取token
this.getPCUserToken(one, scene, result);
return Result.OK("扫码登录成功", result);
} catch (Exception e) {
log.error("扫码登录失败", e);
return Result.error("扫码登录失败");
}
}
/**
* 授权登录获取token
*/
private JSONObject getPCUserToken(MiniProgramUser programUser, String scene, JSONObject result) {
String userAuth = programUser.getUserAuth();
result.put("userType", userAuth);
String userId = programUser.getUserId();
SysUser sysUser = sysUserService.getById(userId);
String username = sysUser.getUsername();
String password = sysUser.getPassword();
// 生成token
String token = JwtUtil.sign(username, password);
// 设置token缓存有效时间
redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, token);
redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME * 2 / 1000);
result.put("token", token);
if (StringUtils.isNotBlank(scene)) {
//websocket--PC端页面登录跳转
simpMessagingTemplate.convertAndSendToUser(scene, "/qrCode", JSONObject.toJSONString(result));
}
return result;
}
//websocket--PC端页面登录跳转,前端建立的连接在这里发挥作用PC端跳转
simpMessagingTemplate.convertAndSendToUser(scene, "/qrCode", JSONObject.toJSONString(result));
springboot webSocket SimpMessagingTemplate单点消息详细介绍
WebStompConfig
package com.lexi.manage.xlink.stomp;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
/**
* EnableWebSocketMessageBroker 注解表明: 这个配置类不仅配置了 WebSocket,还配置了基于代理的 STOMP 消息;
* registerStompEndpoints() 方法:添加一个服务端点,来接收客户端的连接。将 “/chat” 路径注册为 STOMP 端点。这个路径与之前发送和接收消息的目的路径有所不同,
* 这是一个端点,客户端在订阅或发布消息到目的地址前,要连接该端点,即用户发送请求 :url=’/127.0.0.1:8080/chat’ 与 STOMP server
* 进行连接,之后再转发到订阅url; configureMessageBroker() 方法:配置了一个 简单的消息代理,通俗一点讲就是设置消息连接请求的各种规范信息。
*
* @author linyun
* @date 2018/9/13 下午5:15
*/
@Configuration
@EnableWebSocketMessageBroker
public class WebStompConfig implements WebSocketMessageBrokerConfigurer {
@Autowired
private WebStompInterceptor interceptor;
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 添加一个/chat端点,客户端就可以通过这个端点来进行连接;withSockJS作用是添加SockJS支持
registry.addEndpoint("/xlink").setAllowedOrigins("*").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 定义了两个客户端订阅地址的前缀信息,也就是客户端接收服务端发送消息的前缀信息
// device 用于一对一的发送
registry.enableSimpleBroker( "/users");
// 定义了服务端接收地址的前缀,也即客户端给服务端发消息的地址前缀
registry.setUserDestinationPrefix("/users/");
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
// 注册了一个接受客户端消息通道拦截器
registration.interceptors(interceptor);
}
}
//shiro拦截器中此映射加入白名单
filterChainDefinitionMap.put("/xlink/**", "anon");//xlink
以下对应前端页面
// 添加一个/chat端点,客户端就可以通过这个端点来进行连接;withSockJS作用是添加SockJS支持
registry.addEndpoint("/xlink").setAllowedOrigins("*").withSockJS();
// device 用于一对一的发送
registry.enableSimpleBroker( "/users");
// 定义了服务端接收地址的前缀,也即客户端给服务端发消息的地址前缀
registry.setUserDestinationPrefix("/users/");
WebStompInterceptor
package com.lexi.manage.xlink.stomp;
import com.sun.security.auth.UserPrincipal;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.security.Principal;
/**
* @author linyun
* @date 2018/9/13 下午5:57
*/
@Component
public class WebStompInterceptor implements ChannelInterceptor {
/**
* 绑定user到websocket conn上
*
* @param message
* @param channel
* @return
*/
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
String token = accessor.getFirstNativeHeader("xlinkToken");
//System.out.print(token);
if (StringUtils.isEmpty(token)) {
return null;
}
// 绑定user
Principal principal = new UserPrincipal(token);
accessor.setUser(principal);
}
return message;
}
}
后端代码如下:
小程序本地缓存失效情况下,需要授权登录微信获取客户唯一识别码openid
ssself._post_form('/base/shopxo/user/PCScanLogin', transformdata, function(res)
/**
* 微信小程序PC端扫码授权登录
*
* @param jsonObject
* @return
*/
@PostMapping(value = "/PCScanLogin")
public Result<?> PCScanLogin(@RequestBody JSONObject jsonObject) {
JSONObject result = new JSONObject();
try {
String code = jsonObject.getString("JSCODE");
String scene = jsonObject.getString("scene");
Object sceneObj = redisUtil.get(scene);
if (sceneObj == null) {
return Result.error("小程序码已经失效,请重新加载二维码");
}
JSONObject objectResult = weiXinConfigComponent.code2Session(code);
String openid = objectResult.getString("openid");
String session = objectResult.getString("session_key");
result.put("session_key", session);
result.put("openid", openid);
LambdaQueryWrapper<MiniProgramUser> lambdaQuery = new LambdaQueryWrapper<>();
lambdaQuery.eq(MiniProgramUser::getOpenId, openid);
MiniProgramUser one = miniProgramUserService.getOne(lambdaQuery);
if (oConvertUtils.isEmpty(one)) {
result.put("state", "binding");
} else {
result.put("state", "login");
//授权登录获取token
this.getPCUserToken(one, scene, result);
}
return Result.OK(result);
} catch (Exception e) {
e.printStackTrace();
if (e.getMessage().contains("客户登录权限")) {
return Result.error(e.getMessage());
}
return Result.error("网络错误!");
}
}
以上即为PC端扫码登录流程及代码展示。