摘要: 最近工作中需要完成一个APP(不是微信)扫码登录的功能,在APP请求服务器验证成功后,服务器需要推送消息到页面,通知页面登录跳转。 websocket是H5的新特性,虽然08年就已经诞生,但是到现在才被真正的推广。网页版的长连接 web的服务器推送技术 比以前使用ajax轮训性能更高
注意:必须要spring4.0 以上的版本的才能整合websocket
本篇文章中使用spring mvc作为视图框架
实现扫码登录设计了三个接口,其中两个是 HTTP协议的:
http接口1 获取二维码图片
/** * * @Title:获取登录二维码图片 * @Description:Comment for non-overriding methods * @author 张颖辉 * @date 2018年3月5日下午1:57:44 * @param wsSessionId * websocket 会话id (通知页面登录成功的连接) * @param request */ @RequestMapping("getQrCodeImg") public void getQrCodeImg(String wsSessionId, HttpServletRequest request, HttpServletResponse response) { ServletOutputStream outputStream=null; // http://ip:80/LoginMS/qrLogin?appid=?&json={"data":[{ED:"rigour2046"}]} try { String serverUrl = ServerUtil.getServerUrl(request); String qrLoginUrl = serverUrl + "/qrLogin";// http://192.168.1.102:8080/ssoServer/auth/qrCodeLogin qrLoginUrl += "?appid=?"; String clientId = request.getSession().getId() + ":" + wsSessionId; qrLoginUrl += "&json={\"data\":[{ED:\"" + clientId + "\"}]}";// 生成的二维码有data,但是APP发送验证的时候没有data这一层,很尴尬 // http://192.168.1.102:8080/ssoServer/auth/qrCodeLogin?appid=?&json={"data":[{ED:"B49FBCF0B12A06899BF338F9ACE0CC52"}]} logger.debug("扫码登录地址:" + qrLoginUrl); // 内容所使用编码(有中文则必须指定编码) Map<EncodeHintType, String> hints = new HashMap<EncodeHintType, String>(); hints.put(EncodeHintType.CHARACTER_SET, "UTF8"); try { BitMatrix bitMatrix = new MultiFormatWriter().encode(qrLoginUrl, BarcodeFormat.QR_CODE, 250, 250, hints); outputStream = response.getOutputStream(); EWCodeUtil.writeToStream(bitMatrix, "jpg", outputStream); outputStream.flush(); outputStream.close(); } catch (WriterException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }finally { try { outputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } catch (UnknownHostException e) { e.printStackTrace(); } }
依赖:
<!-- 二维码依赖 --> <dependency> <groupId>com.google.zxing</groupId> <artifactId>core</artifactId> <version>3.3.0</version> </dependency>
其中二维码工具类:
package rg.sso.util;import java.awt.image.BufferedImage;import java.io.File;import java.io.IOException;import java.io.OutputStream;import java.util.HashMap;import java.util.Map;import javax.imageio.ImageIO;import com.google.zxing.BarcodeFormat;import com.google.zxing.EncodeHintType;import com.google.zxing.MultiFormatWriter;import com.google.zxing.common.BitMatrix;/** * 二维码工具类 * @Title:EWCodeUtil * @Description:Comment for created type * @author 张颖辉 * @date 2016-12-13下午03:00:33 * @version 1.0 */public class EWCodeUtil { private static final int BLACK = 0Xff000000; private static final int WHITE = 0xffffffff; public EWCodeUtil() { } public static BufferedImage toBufferedImage(BitMatrix matrix) { int width = matrix.getWidth(); int height = matrix.getHeight(); BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { image.setRGB(x, y, (matrix.get(x, y) ? BLACK : WHITE)); } } return image; } /** * * @param matrix 字节阵列 * @param format 图片格式 * @param file 图片文件(传入文件名称,路径) * @throws IOException */ public static void writeToFile(BitMatrix matrix, String format, File file) throws IOException { BufferedImage image = toBufferedImage(matrix); if (!ImageIO.write(image, format, file)) { throw new IOException("Could not write an image of format " + format + " to " + file); } } /** * * @param matrix 字节阵列 * @param format 图片格式 * @param stream 图片文件流 * @throws IOException */ public static void writeToStream(BitMatrix matrix, String format, OutputStream stream) throws IOException { BufferedImage image = toBufferedImage(matrix); if (!ImageIO.write(image, format, stream)) { throw new IOException("Could not write an image of format " + format); } } public static void main(String[] args) { try { String content = "http://jingyan.baidu.com/article/915fc4149e1f9a51394b2007.html"; // String path = "D:"; String path = "D:/"; MultiFormatWriter multiFormatWriter = new MultiFormatWriter(); Map<EncodeHintType, String> hints = new HashMap<EncodeHintType, String>(); // 内容所使用编码(有中文则必须指定编码) hints.put(EncodeHintType.CHARACTER_SET, "UTF8"); BitMatrix bitMatrix = multiFormatWriter.encode(content, BarcodeFormat.QR_CODE, 200, 200, hints); // 生成二维码 File outputFile = new File(path, "张颖辉.jpg"); EWCodeUtil.writeToFile(bitMatrix, "jpg", outputFile); } catch (Exception e) { e.printStackTrace(); } } }
其中 ServerUtil
package rg.sso.util;import java.net.UnknownHostException;import javax.servlet.http.HttpServletRequest;public class ServerUtil { /** * 获取本机的服务器地址(以客户端访问的ip为主) * * @Title:函数 * @Description:Comment for non-overriding methods * @author 张颖辉 * @date 2018年3月5日上午11:13:37 * @return * @throws UnknownHostException */ public static String getServerUrl(HttpServletRequest request) throws UnknownHostException { String serverUrl = null; StringBuffer requestURL = request.getRequestURL(); serverUrl = requestURL.substring(0, requestURL.lastIndexOf("/")); return serverUrl; } }
http接口2 APP发送验证登录信息
/** * * @Title:二维码登录接口 * @Description:APP扫一扫后 请求服务器 二维码登录接口 * @author 张颖辉 * @date 2018年3月5日上午10:38:57 * @param userid * 用户主键 * @param secretType * 秘钥类型:1:手机号 2:邮箱 * @param secretKey * 秘钥 Userid+手机号/邮箱的md5 * @param equipId * 设备号,二维码唯一标识(来自二维码的回传) * @return */ // http://192.168.1.110:8080/ssoServer/auth/qrCodeLogin?appid=8a7d0ec2c8184d2aad329c55259bdee4&json={ED:"111111",userid:"00126ac4091749fe8da5e6745d155e7a"} @RequestMapping("qrLogin") // CBSA要求必须使用这个名称 public void qrCodeLogin(String appid, String json, HttpServletRequest request, HttpServletResponse response) { response.setHeader("Content-type", "text/html;charset=UTF-8"); // TODO 关于appid登录渠道暂时不处理 try { // 解析json参数 ServletOutputStream outputStream = response.getOutputStream(); // JSONObject 可以解析key缺少双引号的jaon JSONObject jsonObject = JSONObject.parseObject(json); if (json == null || jsonObject == null) { // 因为APP已经完成智能接收这样格式的信息,这里就不单独分装返回的类了 outputStream.write("{success:false,msg:'json不能为空'}".getBytes()); outputStream.flush(); outputStream.close(); return; } String userid = jsonObject.getString("userid"); String equipmentId = jsonObject.getString("ED"); if (StringUtil.isEmpty(userid)) { outputStream.write("{success:false,msg:'userId不能为空'}".getBytes()); outputStream.flush(); outputStream.close(); return; } if (StringUtil.isEmpty(equipmentId)) { outputStream.write("{success:false,msg:'ED不能为空'}".getBytes()); outputStream.flush(); outputStream.close(); return; } // 保存user到http会话 User user = userService.selectUserById(userid); if (user == null) { outputStream.write("{success:false,msg:'user不存在'}".getBytes()); outputStream.flush(); outputStream.close(); return; } String sessionId = equipmentId.split(":")[0]; String wsId = equipmentId.split(":")[1]; GlobalSessionCache.getInstance().get(sessionId).setAttribute("user", user); // request.getSession().setAttribute("user", user); // 【推送】通知页面跳转 Msg4Ws<Boolean> msg4Ws = new Msg4Ws<Boolean>(10001, "登录成功", true); String message = new ObjectMapper().writeValueAsString(msg4Ws); // 转为json websocketHandler.sendMsgToUser(wsId, new TextMessage(message)); logger.info("推送发送成功"); // 返回给手机端 outputStream.write("{success:true,msg:'验证成功啦!'}".getBytes()); outputStream.flush(); outputStream.close(); } catch (IOException e) { e.printStackTrace(); }
【重点】websocket接口
<!-- spring-websocket --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-websocket</artifactId> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-messaging</artifactId> </dependency>
在spring的配置文件头部beans标签属性中添加xsd引用:
xmlns:websocket="http://www.springframework.org/schema/websocket"
和
http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket.xsd
然后在配置文件beans标签内添加websocket配置的节点:
<!-- websocket --> <bean id="websocket" class="rg.sso.websocket.handler.WebsocketHandler"/> <websocket:handlers allowed-origins="*"> <websocket:mapping path="/websocket" handler="websocket"/> <websocket:handshake-interceptors> <bean class="rg.sso.websocket.HandshakeInterceptor"/> </websocket:handshake-interceptors> </websocket:handlers>
说明:WebsocketHandler 类是处理消息的类,
HandshakeInterceptor 是握手的拦截器
path="/websocket" 表示请求路径
allowed-origins="*" 表示允许使用任何IP 域名来访问
HandshakeInterceptor 类:
package rg.sso.websocket;import java.util.Map;import org.springframework.http.server.ServerHttpRequest;import org.springframework.http.server.ServerHttpResponse;import org.springframework.web.socket.WebSocketHandler;import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;/** * * @Title:HandshakeInterceptor 握手拦截器 * @Description:Comment for created type * @author 张颖辉 * @date 2018年3月5日下午2:31:35 * @version 1.0 */public class HandshakeInterceptor extends HttpSessionHandshakeInterceptor { @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception { System.out.println("Before Handshake"); return super.beforeHandshake(request, response, wsHandler, attributes); } @Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception ex) { System.out.println("After Handshake"); super.afterHandshake(request, response, wsHandler, ex); } }
WebsocketHandler 类:
package rg.sso.websocket.handler;import java.io.IOException;import java.util.HashMap;import java.util.Map;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.web.socket.CloseStatus;import org.springframework.web.socket.TextMessage;import org.springframework.web.socket.WebSocketMessage;import org.springframework.web.socket.WebSocketSession;import org.springframework.web.socket.handler.TextWebSocketHandler;import com.fasterxml.jackson.databind.ObjectMapper;import rg.sso.vo.Msg4Ws;/** * * @Title:WebsocketEndPoint 消息处理根类 * @Description:Comment for created type * @author 张颖辉 * @date 2018年3月5日下午2:31:48 * @version 1.0 */public class WebsocketHandler extends TextWebSocketHandler { private static Logger logger = LoggerFactory.getLogger(WebsocketHandler.class); // 保存所有的用户session private static Map<String, WebSocketSession> SESSION_MAP = new HashMap<String, WebSocketSession>(); // 处理信息 @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { //super.handleTextMessage(session, message); String msgStr = message.getPayload(); logger.info("收到消息:" + message); logger.info("收到消息内容:" + msgStr); String resultMsg = msgStr + " received at server"; logger.info("返回信息:" + resultMsg); TextMessage returnMessage = new TextMessage(resultMsg); session.sendMessage(returnMessage); } // 连接 就绪时 @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { logger.debug("[{} : {}] has be connected...", session.getUri(), session.getId()); SESSION_MAP.put(session.getId(), session); logger.info("返回id"); Msg4Ws<String> msg4Ws=new Msg4Ws<String>(10000, "ws的sessionId", session.getId()); String message = new ObjectMapper().writeValueAsString(msg4Ws); // 转为json TextMessage returnMessage = new TextMessage(message); session.sendMessage(returnMessage); } // 关闭 连接时 @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { logger.debug("[{} : {}]", session.getUri(), session.getId()); SESSION_MAP.remove(session.getId()); } // 处理传输时异常 @Override public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { // TODO Auto-generated method stub super.handleTransportError(session, exception); } // 支持局部消息 @Override public boolean supportsPartialMessages() { // TODO Auto-generated method stub return super.supportsPartialMessages(); } /** * * @Title:向所有用户推送消息 * @Description:Comment for non-overriding methods * @author 张颖辉 * @date 2018年3月5日下午3:47:50 * @param message * @throws Exception */ public void sendMsgToAllUsers(WebSocketMessage<?> message) throws Exception { for (WebSocketSession user : SESSION_MAP.values()) { user.sendMessage(message); } } /** * @Title:向一个用户推送消息 * @Description:Comment for non-overriding methods * @author 张颖辉 * @date 2018年3月5日下午3:47:32 * @param wsSessionId * @param message * @throws IOException */ public void sendMsgToUser(String wsSessionId, WebSocketMessage<?> message) throws IOException { WebSocketSession session = SESSION_MAP.get(wsSessionId); if (session == null) { throw new RuntimeException("对应sessionid(" + wsSessionId + ")的session不存在"); } session.sendMessage(message); } }
这样,当tomcat 启动后,websocket就启动监听客户端连接了。
前端测试页面中的关键js:
//websocket ---START var basePath = '${basePath}';//http://192.168.1.110:8080/ssoServer/ //var url = "ws://localhost:8080/ssoServer/websocket"; var url = basePath.replace('http', 'ws') + "websocket"; //alert(url); var ws = new WebSocket(url); ws.addEventListener('open', function(ev) { console.log('websocket已经连接'); ws.send('Hello'); }); ws.onmessage = function(evt) { console.log("Received Message: " + evt.data); //{"code":10001,"msg":"登录成功","data":true} var json = evt.data; var hasCode = json.match("code"); if (hasCode == null) { return; } var dataObj = eval("(" + json + ")"); var code = dataObj.code; console.log("code=" + code); if (code == "10000") { //document.getElementById("loginQrImg").src="auth/getQrCodeImg?wsSessionId="+; $("#loginQrImg").attr("src", "auth/getQrCodeImg?wsSessionId=" + dataObj.data); } else if (code == "10001") { ws.close(); location.reload(); } }; //websocket ---END
websocket的消息分装
package rg.sso.vo;import java.io.Serializable;/** * @Title:WebSocket 消息 * @Description:Comment for non-overriding methods * @author 张颖辉 * @date 2018年2月9日下午1:35:49 */public class Msg4Ws<T> implements Serializable { private static final long serialVersionUID = 1L; private int code;//消息编号 private String msg;//消息描述 private T data;//消息实体 public Msg4Ws(int code) { this.code = code; } public Msg4Ws(int code, T data) { this.code = code; this.data = data; } public Msg4Ws(int code, String msg, T data) { this.code = code; this.msg = msg; this.data = data; } public Msg4Ws(int code, String msg) { this.code = code; this.msg = msg; } public int getCode() { return code; } public void setCode(int code) { this.code = code; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public T getData() { return data; } public void setData(T data) { this.data = data; } }
以上是使用了spring 分装之后的webcoket
还有 原生的 javax.websocket
另外 spring webSocket 中获取 httpsession的解决办法
作者:颖辉小居
来源:https://my.oschina.net/iyinghui/blog/1630321