SpringBoot+WebSocket+Redis控制二维码失效时间
腾讯文档采用的是轮询的方式,我在这里采用websocket的方式.
这里运用了github上基于微信SDK的更易用的SDK weixin-java-miniapp
第一步:导入需要的maven依赖
<!--springboot整合了websocket-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-miniapp</artifactId>
<version>3.3.0</version>
</dependency>
第二步:配置websocket
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
这里的socketKey对应@PathParam定义的名字,用来识别唯一的socket连接
@Component
@ServerEndpoint(value = "/socketLogin/{socketKey}")
public class LoginSocket {
private static Logger log= LoggerFactory.getLogger(LoginSocket.class);
private static ConcurrentHashMap<String, Session> sessionMap = new ConcurrentHashMap<>();
private Session session;
/**
* 连接建立成功调用的方法*/
@OnOpen
public void onOpen(Session session, @PathParam("socketKey")String socketKey) {
this.session=session;
log.info("[微信小程序websocket]socketKey:{}",socketKey +"-->建立连接");
sessionMap.put(socketKey,session);
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose(@PathParam("socketKey") String socketKey) {
log.info("[微信小程序websocket]socketKey:{}",socketKey +"-->断开连接");
sessionMap.remove(socketKey);
}
public ConcurrentHashMap<String, Session> getSessionMap() {
return sessionMap;
}
}
第三步:完成socket配置后,正式开始微信小程序开发
开发之前需要准备好申请的小程序APPID SECRET
编写工具类(整合weixin-java-miniapp),这个工具类根据自身需求,灵活设置WxMaInMemoryConfig里面的内容
@Configuration
public class WxMaConfiguration {
private static String appId;
private static String secret;
@Value("${weixin.applet_appid}")
public void setAppId(String appId) {
WxMaConfiguration.appId = appId;
}
@Value("${weixin.applet_secret}")
public void setSecret(String secret) {
WxMaConfiguration.secret = secret;
}
private static WxMaService wxMaService=null;
@Bean
public Object services(){
WxMaInMemoryConfig config = new WxMaInMemoryConfig();
config.setAppid(appId);
config.setSecret(secret);
wxMaService = new WxMaServiceImpl();
wxMaService.setWxMaConfig(config);
return Boolean.TRUE;
}
public static WxMaService getWxMaService(){
return wxMaService;
}
}
第四步:网页端获取小程序二维码接口
建议小程序二维码里面的scene参数和建立websocket连接的key保持一致
pathStr:小程序登录成功后微信小程序的跳转地址,而不是网页的跳转地址.如:pages/index/index
注意此处:3.3.0之前版本的 weixin-java-miniapp无法通过createWxaCodeUnlimitBytes返回byte字节,返回的都是File文件
/**
* 获取微信小程序带参数的二维码
*
* @return
*/
@RequestMapping("/getAppletQrCode")
public void getAppletCode(@RequestParam("sceneStr") String sceneStr, @RequestParam("pathStr") String pathStr, HttpServletResponse response) {
logger.info("[微信小程序]获取微信小程序二维码,参数->sceneStr:"+sceneStr+", pathStr:"+pathStr);
WxMaService wxMaService = WxMaConfiguration.getWxMaService();
// 获取小程序二维码生成实例
WxMaQrcodeService wxMaQrcodeService = wxMaService.getQrcodeService();
// 设置小程序二维码线条颜色为黑色
WxMaCodeLineColor lineColor = new WxMaCodeLineColor("0", "0", "0");
// 生成二维码图片字节流
byte[] qrCodeBytes = null;
try {
qrCodeBytes = wxMaQrcodeService.createWxaCodeUnlimitBytes(sceneStr, pathStr, 430, false, lineColor, false);
//设置二维码180s失效
redisUtils.setex("qrcode:"+sceneStr,3*60,sceneStr);
} catch (Exception e) {
logger.error("[微信小程序]生成小程序码出现异常:{}",e);
}
response.setContentType("image/png");
//写入response的输出流中
try {
OutputStream outputStream = response.getOutputStream();
outputStream.write(Base64.encodeBase64(qrCodeBytes));
outputStream.flush();
outputStream.close();
} catch (Exception e) {
logger.error("[微信小程序]输出流写出小程序码出现异常:{}",e);
}
}
第五步:小程序登录接口,对于前端来说以上接口都在网页端调取,而此接口在小程序端调取,要做好兼容
/**
* 初次未授权登录
* 微信小程序授权登录
*
* @param code
* @return https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code
*/
@RequestMapping("/weChatAppletLogin")
@ResponseBody
public ResultBean weChatAuthLogin(@RequestParam("code") String code, @RequestParam("encryptedData") String encryptedData, @RequestParam("ivStr") String ivStr, @RequestParam("nickname") String nickname, @RequestParam("headurl") String headurl, @RequestParam("socketKey") String socketKey,HttpServletResponse response) {
logger.info("[微信小程序]微信小程序和网页端登录,参数->code:"+code+", encryptedData:"+encryptedData+", nickname:"+nickname+", headurl:"+headurl+", socketKey:"+socketKey);
if (StringUtils.isBlank(code)) {
return ResultBean.setError(1, "code码为空");
}
//微信接口参数
Map<String, String> params = new HashMap<>();
params.put("appid", APPLET_APPID);
params.put("secret", APPLET_SECRET);
params.put("js_code", code);
params.put("grant_type", "authorization_code");
try {
String wxResult = HttpUtil.doGet(APPLET_URL, params);
JSONObject jsonObject = JSONObject.parseObject(wxResult);
if (jsonObject.get("errcode") != null && StringUtils.equalsIgnoreCase(jsonObject.get("errcode").toString(), "40163")) {
return ResultBean.setError(1, "code码失效");
}
String session_key = jsonObject.get("session_key").toString();
//第一次登录没有unionid
Object unionidObj = jsonObject.get("unionid");
Map<String,String> userInfo=null;
String unionid=null;
//第一次授权
if (unionidObj == null) {
userInfo = getAuthUnionId(session_key, encryptedData, ivStr);
unionid = userInfo.get("unionid");
}else{
unionid=jsonObject.get("unionid").toString();
}
WXUser wxUser = wxService.findWXUserByunionid(unionid);
WXUser user = new WXUser();
user.setUnionid(unionid);
user.setNickname(nickname);
user.setHeadimgurl(headurl);
//微信信息存在
if (wxUser != null) {
wxService.updateCloudWXUser(user);
} else {
//第一次登陆
wxService.addWXUser(user);
}
Map<String, String> result = new HashMap<>();
result.put("unionid", unionid);
//设置小程序的登录状态缓存(若不设置,微信默认30天内没有操作就重新登录)
//redisUtils.setex("applet:"+unionid,30*24*60*60,unionid);
//这是网页端的socket返回数据
ConcurrentHashMap<String, Session> sessionMap = loginSocket.getSessionMap();
if (StringUtils.isNotBlank(socketKey)) {
Session currentSession = sessionMap.get(socketKey);
if (currentSession != null) {
String codeKey = redisUtils.getString("qrcode:" + socketKey);
ResultBean resultBean=null;
if(StringUtils.isBlank(codeKey)){
resultBean = ResultBean.setOk(1, "二维码已失效");
}else{
//扫描后删除缓存的二维码
redisUtils.delString("qrcode:" + socketKey);
//设置网页端登录有效时长为6小时
redisUtils.setex(unionid, 3600 * 6, unionid);
resultBean = ResultBean.setOk(0, "扫码登录成功",unionid);
}
String res = JSON.toJSONString(resultBean);
currentSession.getAsyncRemote().sendText(res);
}
}
return ResultBean.setOk(0, "授权登录小程序成功", result);
} catch (Exception e) {
logger.error("[微信小程序]小程序授权登录出现异常:{}",e);
return ResultBean.setError(1, "授权登录小程序失败");
}
}
/**
* 授权过后的再次免授权登录
*
* @param sessionKey
* @param encryptedData
* @param ivStr
* @return 利用微信工具sdk对获取的用户信息解密
*/
public Map<String,String> getAuthUnionId(String sessionKey, String encryptedData,String ivStr) {
try {
WxMaService wxMaService = WxMaConfiguration.getWxMaService();
WxMaUserService userService = new WxMaUserServiceImpl(wxMaService);
WxMaUserInfo userInfo = userService.getUserInfo(sessionKey, encryptedData, ivStr);
String unionId = userInfo.getUnionId();
String nickName = userInfo.getNickName();
String avatarUrl = userInfo.getAvatarUrl();
Map<String, String> userInfoMap = new HashMap<>();
userInfoMap.put("unionid", unionId);
userInfoMap.put("nickname", nickName);
userInfoMap.put("headurl",avatarUrl);
return userInfoMap;
} catch (Exception e) {
logger.error("[微信小程序]已授权登录出现异常:{}",e);
return null;
}
}
前端demo
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="http://libs.baidu.com/jquery/1.9.1/jquery.js"></script>
<script type="text/javascript">
$(function () {
var ip="xxx.xxx.xxx.xxx";
var sceneStr = "scend-" + new Date().getTime() + Math.ceil(Math.random() * 888888 + 1000000);
//建立websocket192.168.101.123
//var s = encodeURIComponent(sceneStr);
var ws = new WebSocket("ws://"+ip+":端口号/项目名(没有不写)/websocket路径/" + sceneStr)
ws.onopen = function () {
console.log("websocket建立连接")
}
ws.onmessage = function (ev) {
var parse = JSON.parse(ev.data);
console.log(parse)
var code1 = parse.code;
if(code1==0){
ws.onclose=function () { console.log("websocket连接关闭")}
window.location.href="http://www.baidu.com";
}
}
var url="小程序码接口地址";
var pathStr="pages/index/index";
$.post(url,{
sceneStr: sceneStr,
pathStr: pathStr
},function(result){
$("#qrcode").attr("src","data:image/png;base64,"+result);
})
})
</script>
</head>
<body>
<img id="qrcode" src="">
</body>
</html>
小程序端demo(此demo仅做参考)
import {
Base64,
Crypto
} from './utils/ossUpload';
import {
format
} from './utils/util.js';
App({
onLaunch: function(option) {
this.onLaunchArgu = option;
console.log(option)
this.init();
this.globalData.os = this._getSystemOs()
console.log(this._getSystemInfo())
},
requestArr:[],
// 初始化
init(fallback){
wx.checkSession({
success: () => {
console.log('登录没过期了')
// 获取用户信息
wx.getSetting({
success: res => {
if (res.authSetting['scope.userInfo']) {
// 已经授权,可以直接调用 getUserInfo 获取头像昵称,不会弹框
this.getUserInfo().then(res => {
this.globalData.userInfo = res.userInfo
// 由于 getUserInfo 是网络请求,可能会在 Page.onLoad 之后才返回
// 所以此处加入 callback 以防止这种情况
if (this.userInfoReadyCallback) {
this.userInfoReadyCallback(res)
}
}, () => {})
}
}
})
},
fail(res) {
console.log('登录过期了');
},
complete(res) {
console.log(res);
}
})
},
login(callback) {
wx.login({
success: res => {
const code = res.code;
this.getUserInfo().then(res => {
this.request({
url: '/studyassistant/weixin/weChatAppletLogin',
data: {
#这里的参数是扫二维码获取到的,方便调试,直接拿取
socketKey: this.onLaunchArgu.query.scene,
code,
encryptedData: res.encryptedData,
ivStr: res.iv,
headurl: res.userInfo.avatarUrl,
nickname: encodeURIComponent(res.userInfo.nickName)
}
}, false).then(res => {
this.globalData.unionId = res.data.unionid;
this._setStorageSync({
unionId: res.data.unionid,
})
if (this.requestArr.length) {
this.requestArr.map((item) => {
this.request(item.option, item.author);
})
this.request()
}
callback();
})
}, () => {})
}
})
},
微信小程序调试
这里的启动参数就是页面上小程序二维码里面的参数scene=(你的参数)
这样调试的时候,就不用了手机扫码了