SpringBoot+WebSocket+Redis控制二維碼失效時(shí)間
類似于騰訊文檔網(wǎng)頁(yè)版掃描小程序碼登錄的模式
騰訊文檔采用的是輪詢的方式,我在這里采用websocket的方式.
這里運(yùn)用了github上基于微信SDK的更易用的SDK weixin-java-miniapp
第一步:導(dǎo)入需要的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對(duì)應(yīng)@PathParam定義的名字,用來(lái)識(shí)別唯一的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;
? ? /**
? ? * 連接建立成功調(diào)用的方法*/
? ? @OnOpen
? ? public void onOpen(Session session, @PathParam("socketKey")String socketKey) {
? ? ? ? this.session=session;
? ? ? ? log.info("[微信小程序websocket]socketKey:{}",socketKey +"-->建立連接");
? ? ? ? sessionMap.put(socketKey,session);
? ? }
? ? /**
? ? * 連接關(guān)閉調(diào)用的方法
? ? */
? ? @OnClose
? ? public void onClose(@PathParam("socketKey") String socketKey) {
? ? ? ? log.info("[微信小程序websocket]socketKey:{}",socketKey +"-->斷開(kāi)連接");
? ? ? ? sessionMap.remove(socketKey);
? ? }
? ? public ConcurrentHashMap<String, Session> getSessionMap() {
? ? ? ? return sessionMap;
? ? }
}
第三步:完成socket配置后,正式開(kāi)始微信小程序開(kāi)發(fā)
開(kāi)發(fā)之前需要準(zhǔn)備好申請(qǐng)的小程序APPID SECRET
編寫工具類(整合weixin-java-miniapp),這個(gè)工具類根據(jù)自身需求,靈活設(shè)置WxMaInMemoryConfig里面的內(nèi)容
@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;
? ? }
}
第四步:網(wǎng)頁(yè)端獲取小程序二維碼接口
建議小程序二維碼里面的scene參數(shù)和建立websocket連接的key保持一致
pathStr:小程序登錄成功后微信小程序的跳轉(zhuǎn)地址,而不是網(wǎng)頁(yè)的跳轉(zhuǎn)地址.如:pages/index/index
注意此處:3.3.0之前版本的 weixin-java-miniapp無(wú)法通過(guò)createWxaCodeUnlimitBytes返回byte字節(jié),返回的都是File文件
/**
? ? * 獲取微信小程序帶參數(shù)的二維碼
? ? *
? ? * @return
? ? */
? ? @RequestMapping("/getAppletQrCode")
? ? public void getAppletCode(@RequestParam("sceneStr") String sceneStr, @RequestParam("pathStr") String pathStr, HttpServletResponse response) {
? ? ? ? logger.info("[微信小程序]獲取微信小程序二維碼,參數(shù)->sceneStr:"+sceneStr+", pathStr:"+pathStr);
? ? ? ? WxMaService wxMaService = WxMaConfiguration.getWxMaService();
? ? ? ? // 獲取小程序二維碼生成實(shí)例
? ? ? ? WxMaQrcodeService wxMaQrcodeService = wxMaService.getQrcodeService();
? ? ? ? // 設(shè)置小程序二維碼線條顏色為黑色
? ? ? ? WxMaCodeLineColor lineColor = new WxMaCodeLineColor("0", "0", "0");
? ? ? ? // 生成二維碼圖片字節(jié)流?
? ? ? ? byte[] qrCodeBytes = null;
? ? ? ? try {
? ? ? ? ? ? qrCodeBytes = wxMaQrcodeService.createWxaCodeUnlimitBytes(sceneStr, pathStr, 430, false, lineColor, false);
? ? ? ? ? ? //設(shè)置二維碼180s失效
? ? ? ? ? ? redisUtils.setex("qrcode:"+sceneStr,3*60,sceneStr);
? ? ? ? } catch (Exception e) {
? ? ? ? ? ? logger.error("[微信小程序]生成小程序碼出現(xiàn)異常:{}",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("[微信小程序]輸出流寫出小程序碼出現(xiàn)異常:{}",e);
? ? ? ? }
? ? }
第五步:小程序登錄接口,對(duì)于前端來(lái)說(shuō)以上接口都在網(wǎng)頁(yè)端調(diào)取,而此接口在小程序端調(diào)取,要做好兼容
/**
? ? * 初次未授權(quán)登錄
? ? * 微信小程序授權(quán)登錄
? ? *
? ? * @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("[微信小程序]微信小程序和網(wǎng)頁(yè)端登錄,參數(shù)->code:"+code+", encryptedData:"+encryptedData+", nickname:"+nickname+", headurl:"+headurl+", socketKey:"+socketKey);
? ? ? ? if (StringUtils.isBlank(code)) {
? ? ? ? ? ? return ResultBean.setError(1, "code碼為空");
? ? ? ? }
? ? ? ? //微信接口參數(shù)
? ? ? ? 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();
? ? ? ? ? ? //第一次登錄沒(méi)有unionid
? ? ? ? ? ? Object unionidObj = jsonObject.get("unionid");
? ? ? ? ? ? Map<String,String> userInfo=null;
? ? ? ? ? ? String unionid=null;
? ? ? ? ? ? //第一次授權(quán)
? ? ? ? ? ? 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);
? ? ? ? ? ? //設(shè)置小程序的登錄狀態(tài)緩存(若不設(shè)置,微信默認(rèn)30天內(nèi)沒(méi)有操作就重新登錄)
? ? ? ? ? ? //redisUtils.setex("applet:"+unionid,30*24*60*60,unionid);
? ? ? ? ? ? //這是網(wǎng)頁(yè)端的socket返回?cái)?shù)據(jù)
? ? ? ? ? ? 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);
? ? ? ? ? ? ? ? ? ? ? ? //設(shè)置網(wǎng)頁(yè)端登錄有效時(shí)長(zhǎng)為6小時(shí)
? ? ? ? ? ? ? ? ? ? ? ? redisUtils.setex(unionid, 3600 * 6, unionid);
? ? ? ? ? ? ? ? ? ? ? ? resultBean = ResultBean.setOk(0, "掃碼登錄成功",unionid);
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? ? ? String res = JSON.toJSONString(resultBean);
? ? ? ? ? ? ? ? ? ? currentSession.getAsyncRemote().sendText(res);
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? ? ? return ResultBean.setOk(0, "授權(quán)登錄小程序成功", result);
? ? ? ? } catch (Exception e) {
? ? ? ? ? ? logger.error("[微信小程序]小程序授權(quán)登錄出現(xiàn)異常:{}",e);
? ? ? ? ? ? return ResultBean.setError(1, "授權(quán)登錄小程序失敗");
? ? ? ? }
? ? }
/**
? ? * 授權(quán)過(guò)后的再次免授權(quán)登錄
? ? *
? ? * @param sessionKey
? ? * @param encryptedData
? ? * @param ivStr
? ? * @return 利用微信工具sdk對(duì)獲取的用戶信息解密
? ? */
? ? 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("[微信小程序]已授權(quán)登錄出現(xiàn)異常:{}",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+":端口號(hào)/項(xiàng)目名(沒(méi)有不寫)/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連接關(guān)閉")}
? ? ? ? ? ? ? ? ? ? window.location.;
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? ? ? 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('登錄沒(méi)過(guò)期了')
? ? ? ? ? // 獲取用戶信息
? ? ? ? ? wx.getSetting({
? ? ? ? ? ? success: res => {
? ? ? ? ? ? ? if (res.authSetting['scope.userInfo']) {
? ? ? ? ? ? ? ? // 已經(jīng)授權(quán),可以直接調(diào)用 getUserInfo 獲取頭像昵稱,不會(huì)彈框
? ? ? ? ? ? ? ? this.getUserInfo().then(res => {
? ? ? ? ? ? ? ? ? this.globalData.userInfo = res.userInfo
? ? ? ? ? ? ? ? ? // 由于 getUserInfo 是網(wǎng)絡(luò)請(qǐng)求窃爷,可能會(huì)在 Page.onLoad 之后才返回
? ? ? ? ? ? ? ? ? // 所以此處加入 callback 以防止這種情況
? ? ? ? ? ? ? ? ? if (this.userInfoReadyCallback) {
? ? ? ? ? ? ? ? ? ? this.userInfoReadyCallback(res)
? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? }, () => {})
? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? ? })
? ? ? },
? ? ? fail(res) {
? ? ? ? console.log('登錄過(guò)期了');
? ? ? },
? ? ? 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: {
? ? ? ? ? ? ? #這里的參數(shù)是掃二維碼獲取到的,方便調(diào)試,直接拿取
? ? ? ? ? ? ? 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();
? ? ? ? ? })
? ? ? ? }, () => {})
? ? ? }
? ? })
? },
微信小程序調(diào)試
這里的啟動(dòng)參數(shù)就是頁(yè)面上小程序二維碼里面的參數(shù)scene=(你的參數(shù))
這樣調(diào)試的時(shí)候,就不用了手機(jī)掃碼了