开发工具:MacOS、IDEA
技术栈:JDK1.8、SpringBoot、Thymeleaf、websocket、ZXing、jjwt
项目简介:
最近在想要打通各个子项目,于是搭建一个统一认证平台就成了任务的核心,对于企业级的CAS认证服务不在考虑范围内,轻量级任务框架如XXL-SSO我比较喜欢,经过一番研究,发现技术落地的核心是SpringBoot,Redis,拦截器。最终决定,自己搭建一个统一认证平台。这一篇文章对于单点登录不做描述,而是针对单点登录下的登录方式之一:扫码。
开发原理:
二维码生成技术使用谷歌开源的ZXing框架
前台采用Thymeleaf模板获取初始化数据
前后端通讯方式采用全双工通信的WebSocket
开发方案
第一步,连接到WebSocket上,获取到二维码。过程如下
前台打开登录界面,首先由SpringBoot的Controoler层分配一唯一UUID(分布式可采用雪花算法生成唯一ID,这里单机所以采用UUID),然后前端携带UUID连接到WebSocket服务中,与此同时异步请求携带UUID请求二维码接口,由接口输出二维码的流到页面上展示。
第二部,扫码,发送授权登录的请求,返回身份Token。过程如下
通过小程序/APP扫描二维码,取到二维码中的UUID,弹出是否授权登录弹窗,如果同意授权,则携带UUID和Token(小程序和APP已经登录过,所有具有身份信息)去请求确认登录的接口,接口通过UUID找到对应WebSocket连接的Session,然后传输Token给前端,如此便登录成功
实现效果
使用postman模拟扫码授权登录
核心代码
ViewController
@Controller public class LoginController { @RequestMapping("login") public String login(HashMap<String, Object> map) { //UUID String code = generateUUID(); map.put("code", code); map.put("url", "/zing/login/" + code); return "login"; } }
HTML(只展示核心)
<!--展示二维码--> <img th:src="${url}" style="width: 188px;height: 188px;border: 1px solid #E2E2E2;"> <!--webSocket连接--> <script> var token; if (typeof (WebSocket) == "undefined") { alert("您的浏览器不支持WebSocket"); } else { socket = new WebSocket("ws://127.0.0.1/websocket/".replace("http", "ws")); socket.onopen = function () { console.log("Socket 已打开"); //发送Code socket.send("[[${code}]]") } socket.onmessage = function (msg) { token = JSON.parse(msg.data).data console.log("Token获取成功:" + token) } socket.onclose = function () { console.log("Socket已关闭"); }; //发生了错误事件 socket.onerror = function () { alert("Socket发生了错误,请刷新"); } } </script>
Rest接口
@RequestMapping("zing/login/{token}") @ResponseBody public void createQRCode(HttpServletResponse response, @PathVariable("token") String token) { //将带有Token的二维码返回到前端 OutputStream oStream = null; try { ByteArrayOutputStream baos = new ByteArrayOutputStream(); //生成二维码 generate(token, baos); byte[] bytes = baos.toByteArray(); oStream = response.getOutputStream(); oStream.write(bytes); } catch (IOException e) { log.error("生成二维码出现错误", e); e.printStackTrace(); } finally { //当创建对象成功时候,在执行close()方法。 if (oStream != null) { try { oStream.close(); } catch (IOException e) { try { oStream.close(); } catch (IOException e1) { log.error("生成二维码出现错误", e); e1.printStackTrace(); } log.error("生成二维码出现错误", e); e.printStackTrace(); } } } } private static void generate(String token, OutputStream stream) { try { MultiFormatWriter multiFormatWriter = new MultiFormatWriter(); FiveElements fiveElements = new FiveElements(); fiveElements.setToken(token); fiveElements.setDate(new Date()); String contents = JSON.toJSONString(fiveElements); HashMap<EncodeHintType, Object> hints = new HashMap<>(); hints.put(EncodeHintType.CHARACTER_SET, CHARTSET); hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M); hints.put(EncodeHintType.MARGIN, 2); BitMatrix bitMatrix = multiFormatWriter.encode(contents, BarcodeFormat.QR_CODE, 400, 400, hints); MatrixToImageWriter.writeToStream(bitMatrix, "jpg", stream); } catch (Exception e) { System.out.println("二维码生成出错" + e.getMessage()); } }
WebSocket
/** * @Auther: 陈龙 * @Date: 2019-07-24 10:43 * @Description: */ @ServerEndpoint("/websocket/") @Component public class WebSocketServer { static Logger log = LoggerFactory.getLogger(WebSocketServer.class); //静态变量,用来记录当前在线连接数。 private static AtomicInteger onlineCount = new AtomicInteger(0); //concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。 private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<>(); //与某个客户端的连接会话,需要通过它来给客户端发送数据 private Session session; private String code; /** * 连接建立成功调用的方法 */ @OnOpen public void onOpen(Session session) { //UUID作为随机Token this.session = session; webSocketSet.add(this);//加入set中 addOnlineCount();//在线数加1 log.info("有新请求链接进入,当前在线人数为" + getOnlineCount()); } /** * 连接关闭调用的方法 */ @OnClose public void onClose() { webSocketSet.remove(this); //从set中删除 subOnlineCount(); //在线数减1 log.info("有一连接关闭!当前在线人数为" + getOnlineCount()); } /** * 收到客户端消息后调用的方法 * * @param message 客户端发送过来的消息 */ @OnMessage public void onMessage(String message, Session session) { log.info("收到来自窗口的信息,Code为:" + message); //群发消息 for (WebSocketServer item : webSocketSet) { if (item.session == item.session) { //存储code item.code = message; break; } } } /** * @param session * @param error */ @OnError public void onError(Session session, Throwable error) { log.error("发生错误"); error.printStackTrace(); } /** * 实现服务器主动推送 */ public void sendMessage(String message) throws IOException { this.session.getBasicRemote().sendText(message); } /** * 群发自定义消息 */ public static void sendInfo(String message, String code) throws IOException { log.info("推送消息到窗口" + code + ",推送内容:" + message); for (WebSocketServer item : webSocketSet) { try { //这里可以设定只推送给这个sid的,为null则全部推送 if (code == null) { item.sendMessage(message); } else if (item.code.equals(code)) { item.sendMessage(message); } } catch (IOException e) { continue; } } } public static int getOnlineCount() { return onlineCount.get(); } public static void addOnlineCount() { WebSocketServer.onlineCount.incrementAndGet(); } public static void subOnlineCount() { WebSocketServer.onlineCount.decrementAndGet(); } }
本文暂时没有评论,来添加一个吧(●'◡'●)