开发工具: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();
}
}