基于Iframe+Postmessage前端实现SSO(SingleSignOn)单点登录功能
概述
- SSO单点登录前端需要实现的主要功能就是一端token,多端可用 ~
- 同时后端基于Oauth2+JWT实现的SSO单点登录功能需要有统一的中转登录页 middle.html
- 触发的场景可以有从中转登录页登录后,通过昆仑跳转只邻汇吧、PMS、location;或者直接通过新窗口打开这些页面
middle.html代码如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录验证中心</title>
<style>
.login-form{
margin: 100px auto;
text-align: center;
}
.login-form button{
width: 200px;
margin-top: 20px;;
}
.login-form p{
margin-top: 20px;
font-size: 24px;;
}
.login-form p.success{
color: lightgreen;
}
.login-form p.fail{
color: orangered;
}
</style>
</head>
<body>
<form class="login-form">
<div>
<label for="username">账号: </label><input type="text" name="username" id="username"/>
</div>
<div>
<label for="password">密码: </label><input type="password" name="password" id="password"/>
</div>
<button type="button" onclick="sendLogin('http://pms.nps.700777.xyz/api/oauth/token', 'post')">登录</button>
<p class="login-info"></p>
</form>
<div class="nav">
<a href="http://127.0.0.1:5500/client1.html">client1</a>
<a href="http://127.0.0.1:5500/client2.html">client2</a>
</div>
<script>
const $ = name => document.querySelector(name);
// 发送登陆请求
function sendLogin(url, method) {
const username = $("#username");
const password = $("#password");
const loginInfo = $(".login-info");
// 服务端需要的数据
let grantType = "password";
let clientId = 2;
let clientSecret = "FSVgqgds7HXyf1tT1wFhkyMlNUoiSsCPZMS9xAkr";
let requestParame = `grant_type=${grantType}&client_id=${clientId}&client_secret=${clientSecret}&scope=*&username=${username.value}&password=${password.value}`
const xhr = new XMLHttpRequest();
xhr.open(method, url, true);
xhr.setRequestHeader("content-type","application/x-www-form-urlencoded");
xhr.send(requestParame);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
let data = JSON.parse(xhr.responseText);
// 将token通过localStorage保存在本地
let token = `${data.token_type} ${data.access_token}`;
localStorageMap.setItem("token", token);
loginInfo.innerText = "登录成功";
loginInfo.className = "login-info success";
if (getQueryOriginUrl()) {
// 若有来源则跳转到来源页
setTimeout(function() {
window.location.href = getQueryOriginUrl();
}, 500)
}
}
else {
loginInfo.innerText = "登录失败";
loginInfo.className = "login-info fail";
}
}
}
// 获取地址栏参数(跳转数据来源)
function getQueryOriginUrl() {
var query = window.location.search.substring(1);
var originUrl = query.split("=")[1];
return decodeURIComponent(originUrl);
}
// localStorage映射关系
let localStorageMap = {
setItem: (key, value) => localStorage.setItem(key, value),
getItem: (key) => localStorage.getItem(key),
removeItem: (key) => localStorage.removeItem(key)
}
// “中转页面”监听 ifameWin.postMessage() 事件
window.addEventListener('message', function (e) {
let { method, key, value, origin } = e.data
// 处理对应的存储方法
let result = localStorageMap[method](key, value)
// 返回给当前 client 的数据
let response = {
result,
}
// 验证是否为信任域
function receiveMessage($origin) {
switch ($origin) {
case "linhuiba":
case "PMS":
case "location":
return $origin;
default:
return false;
}
}
// 把获取的数据,传递给 client 窗口
window.parent.postMessage(response, origin)
})
</script>
</body>
</html>
需要获取登录token信息的client1.html代码如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>客户端1-跨域获取localStorage</title>
<style>
.login-status{
margin-top: 50px;
text-align: center;
font-size: 24px;
}
.login-status.success{
color: lightgreen;
}
.login-status.fail{
color: orangered;
}
</style>
</head>
<body>
<!-- 开始存储事件 -->
<!-- 此次为模拟获取中转页token -->
<button onclick="handleSetLocalStorage('getItem', 'token', 'http://127.0.0.1:5500')">Log In</button>
<button onclick="handleSetLocalStorage('removeItem', 'token', 'http://127.0.0.1:5500')">Log Out</button>
<!-- iframe 嵌套“中转页面” middle.html -->
<iframe src="http://pms.nps.700777.xyz/" frameborder="0" id="middle" hidden></iframe>
<p class="login-status">登录状态</p>
<script>
const $ = name => document.querySelector(name);
// 获取 iframe window 对象
const ifameWin = $('#middle').contentWindow;
// 获取登录状态对象
const loginStatus = $('.login-status');
// 登录中心地址
const loginCenterUrl = 'http://pms.nps.700777.xyz/';
function handleSetLocalStorage (method, key, origin, value="") {
let request = {
// 存储的方法
method,
// 存储的 key
key,
// 设置当前域
origin,
// 存储的 value
value
}
// 向 iframe “中转页面”发送消息
ifameWin.postMessage(request, loginCenterUrl)
}
window.addEventListener('message', function(e) {
console.log('我收到了', e.data);
// 设置token到当前域localStorage中
localStorage.setItem('token', e.data.result);
if (!e.data.result) {
loginStatus.innerText = "未登录,即将跳转至登录中心";
loginStatus.className = "login-status fail";
setTimeout(function() {
window.location.href = `${loginCenterUrl}?from=${encodeURIComponent('http://127.0.0.1:5500/client1.html')}`;
}, 1500);
}
else {
loginStatus.innerText = "已登录";
loginStatus.className = "login-status success";
}
})
</script>
</body>
</html>
需要获取登录token信息的client2.html代码如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>客户端2-跨域获取localStorage</title>
</head>
<body>
<!-- 获取本地存储数据 -->
<button onclick="handleGetItem()">client2-getItem</button>
<!-- iframe 嵌套“中转页面” middle.html -->
<iframe src="http://pms.nps.700777.xyz/" frameborder="0" id="middle" hidden></iframe>
<script>
const $ = name => document.querySelector(name);
// 获取 iframe window 对象
const ifameWin = $('#middle').contentWindow;
// 登录中心地址
const loginCenterUrl = 'http://pms.nps.700777.xyz/';
function handleSetLocalStorage (method, key, origin, value="") {
let request = {
// 存储的方法
method,
// 存储的 key
key,
// 设置当前域
origin,
// 存储的 value
value
}
// 向 iframe “中转页面”发送消息
ifameWin.postMessage(request, loginCenterUrl)
}
// 监听 iframe “中转页面”返回的消息
window.addEventListener('message', function (e) {
console.log('client 2 获取到数据啦:', e.data.result);
// 设置token到当前域localStorage中
localStorage.setItem('token', e.data.result);
})
</script>
</body>
</html>