摘要: 最近工作中需要完成一个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 ---ENDwebsocket的消息分装
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

随时随地看视频