首先我们再次复习一下,多个系统之间为什么无法同步登录状态?
- 前端的
Token
无法在多个系统下共享。 - 后端的
Session
无法在多个系统间共享。
关于第二点,我们已在 "SSO模式一" 章节中阐述,使用 Alone独立Redis插件 做到权限缓存直连 SSO-Redis 数据中心,在此不再赘述。
而第一点,才是我们解决问题的关键所在,在跨域模式下,意味着 "共享Cookie方案" 的失效,我们必须采用一种新的方案来传递Token。
- 用户在 子系统 点击
[登录]
按钮。 - 用户跳转到子系统登录接口
/sso/login
,并携带back参数
记录初始页面URL。- 形如:
http://{sso-client}/sso/login?back=xxx
- 形如:
- 子系统检测到此用户尚未登录,再次将其重定向至SSO认证中心,并携带
redirect参数
记录子系统的登录页URL。- 形如:
http://{sso-server}/sso/auth?redirect=xxx?back=xxx
- 形如:
- 用户进入了 SSO认证中心 的登录页面,开始登录。
- 用户 输入账号密码 并 登录成功,SSO认证中心再次将用户重定向至子系统的登录接口
/sso/login
,并携带ticket码
参数。- 形如:
http://{sso-client}/sso/login?back=xxx&ticket=xxxxxxxxx
- 形如:
- 子系统根据
ticket码
从SSO-Redis
中获取账号id,并在子系统登录此账号会话。 - 子系统将用户再次重定向至最初始的
back
页面。
整个过程,除了第四步用户在SSO认证中心登录时会被打断,其余过程均是自动化的,当用户在另一个子系统再次点击[登录]
按钮,由于此用户在SSO认证中心已有会话存在, 所以第四步也将自动化,也就是单点登录的最终目的 —— 一次登录,处处通行。
第6步中,子系统根据 ticket码
从 SSO-Redis
中获取账号id,并在子系统登录此账号会话。详细如下:
public Object checkTicket(String ticket, String currUri) {
SaSsoConfig cfg = SaSsoManager.getConfig();
ApiName apiName = ssoTemplate.apiName;
// --------- 两种模式
if(cfg.getIsHttp()) {
// q1、使用模式三:使用 http 请求从认证中心校验ticket
// 计算当前 sso-client 的单点注销回调地址
String ssoLogoutCall = null;
if(cfg.getIsSlo()) {
// 如果配置了回调地址,就使用配置的值:
if(SaFoxUtil.isNotEmpty(cfg.getSsoLogoutCall())) {
ssoLogoutCall = cfg.getSsoLogoutCall();
}
// 如果提供了当前 uri,则根据此值来计算:
else if(SaFoxUtil.isNotEmpty(currUri)) {
ssoLogoutCall = SaHolder.getRequest().getUrl().replace(currUri, apiName.ssoLogoutCall);
}
// 否则视为不注册单点注销回调地址
else {
}
}
// 发起请求
String checkUrl = ssoTemplate.buildCheckTicketUrl(ticket, ssoLogoutCall);
SaResult result = ssoTemplate.request(checkUrl);
// 校验
if(result.getCode() == SaResult.CODE_SUCCESS) {
return result.getData();
} else {
// 将 sso-server 回应的消息作为异常抛出
throw new SaSsoException(result.getMsg()).setCode(SaSsoErrorCode.CODE_30005);
}
} else {
// q2、使用模式二:直连Redis校验ticket
return ssoTemplate.checkTicket(ticket);
}
}
子系统拿到ticket校验的时候,需要传个client参数,标记自己是谁,避免ticket被中间人拿走用。
public Object checkTicket(String ticket, String client) {
// 读取 loginId
String loginId = SaManager.getSaTokenDao().get(splicingTicketSaveKey(ticket));
if(loginId != null) {
// 如果是 "a,b" 的格式,则解析出对应的 Client
String ticketClient = null;
if(loginId.indexOf(",") > -1) {
String[] arr = loginId.split(",");
loginId = arr[0];
ticketClient = arr[1];
}
// 如果指定了 client 标识,则校验一下 client 标识是否一致
if(SaFoxUtil.isNotEmpty(client) && SaFoxUtil.notEquals(client, ticketClient)) {
throw new SaSsoException("该 ticket 不属于 client=" + client + ", ticket 值: " + ticket)
.setCode(SaSsoErrorCode.CODE_30011);
}
// 删除 ticket 信息,使其只有一次性有效
deleteTicket(ticket);
deleteTicketIndex(loginId);
}
//
return loginId;
}
此外,一般ssoServer要记录回调的host,也就是上文的{sso-client}
,做个防钓鱼操作。
在SSO(Single Sign-On)认证中心回调子系统时,URL中直接携带token的确存在中间人攻击的风险。中间人攻击是指黑客截获用户与认证中心之间的通信,并伪装成用户与子系统之间的通信,从而获取用户的敏感信息或篡改数据。
为了减少中间人攻击的风险,可以采取以下措施:
-
使用HTTPS协议:通过使用HTTPS协议,可以确保通信过程中的数据加密,从而防止中间人窃取token等敏感信息。
-
对token进行签名或加密:在生成token时,可以使用签名或加密算法对其进行处理,确保token在传输过程中不被篡改或窃取。
-
设置token的有效期限:为token设置一个较短的有效期限,以降低被攻击的风险。在token过期后,用户需要重新进行认证。
-
使用防重放攻击措施:在子系统中对token进行校验时,可以使用防重放攻击的措施,如使用随机数或时间戳等方式防止token被重放。
需要注意的是,以上措施可以提高安全性,但无法完全消除中间人攻击的风险。因此,在实际应用中,还需要综合考虑其他安全措施,如使用双因素认证、IP限制等来增加系统的安全性。
可以在token上做文章,例如token是一段密文,能成功解析出用户信息就算验证通过,别人拿了token,也无法从token中解析出当前用户的登录信息。
【可以搞一个心跳cookie】