Shiro单点登录过程和重定向问题分析
[if !supportLists]1. [endif]技术背景
单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
1.1 Session
Http协议是一种无状态协议,即每次服务端接收到客户端的请求时,都是一个全新的请求,服务器并不知道客户端的历史请求记录;Session的主要目的就是为了弥补Http的无状态特性,服务器可以利用session存储客户端在同一个会话期间的一些操作记录;
会话机制:浏览器第一次请求服务器,服务器创建一个会话,并将会话的id作为响应的一部分发送给浏览器,浏览器存储会话id,并在后续第二次和第三次请求中带上会话id,服务器取得请求中的会话id判断是否是同一个用户。
1.1.1 session保存方式
浏览器端:存放在浏览器cookie中,生命周期是session,当浏览器关闭时cookie会消失。(session劫持)。
Tomcat session 以CurrentHashMap的数据结构存储。
tomcat中多个会话对应的session是由ManagerBase类来维护,查看其代码,可以发现其有一个sessions成员属性,存储着各个会话的session信息:
/**
* The set of currently activeSessions for this Manager, keyed by
* session identifier.
*/
protected Map sessions = new ConcurrentHashMap();
1.1.2 Session的生成
Tomcat
session生成器的类实现,SessionIdGeneratorBase,实现代码如下
public String generateSessionId() {
return this.generateSessionId(this.jvmRoute);
}
public String generateSessionId(String route) {
byte[] random =new byte[16];
int sessionIdLength =this.getSessionIdLength();
StringBuilder buffer =new StringBuilder(2 * sessionIdLength + 20);
int resultLenBytes = 0;
while(resultLenBytes < sessionIdLength) {
this.getRandomBytes(random);
for(int j = 0; j < random.length && resultLenBytes < sessionIdLength; ++j) {
byte b1 = (byte)((random[j] & 240) >> 4);
byte b2 = (byte)(random[j] & 15);
if (b1 < 10) {
buffer.append((char)(48 + b1));
}else {
buffer.append((char)(65 + (b1 - 10)));
}
if (b2 < 10) {
buffer.append((char)(48 + b2));
}else {
buffer.append((char)(65 + (b2 - 10)));
}
++resultLenBytes;
}
}
if (route !=null && route.length() > 0) {
buffer.append('.').append(route);
}else {
String jvmRoute =this.getJvmRoute();
if (jvmRoute !=null && jvmRoute.length() > 0) {
buffer.append('.').append(jvmRoute);
}
}
return buffer.toString();
}
1.1.3 session
ID生产算法
Random类中实现的随机算法是伪随机,也就是有规则的随机。在进行随机时,随机算法的起源数字称为种子数(seed),在种子数的基础上进行一定的变换,从而产生需要的随机数字,当种子可知时,随机数也是可知的。
[if !supportLists]· [endif]SecureRandom类提供加密的强随机数生成器(RNG)
使用的随机数生成工具SecureRandom工具生成随机数。
protected void getRandomBytes(byte[] bytes) {
SecureRandom random = (SecureRandom)this.randoms.poll();
if (random ==null) {
random =this.createSecureRandom();
}
random.nextBytes(bytes);
this.randoms.add(random);
}
SecureRandom的实现算法,NativePRNG,否则就是SHA1PRNG。
操作系统收集了一些随机事件,比如鼠标点击,键盘点击等等,SecureRandom 使用这些随机事件作为种子,从而使得种子不可预测。
1.2 单系统登录实现。
1.2.1 会话维护
会话机制中,浏览器第一次请求服务器需要输入用户名与密码验证身份,服务器拿到用户名密码去数据库比对,验证通过则说明当前持有这个会话的用户是合法用户,应该将这个会话标记为“已授权”或者“已登录”等等之类的状态,并将信息保存在session中,tomcat在会话对象中设置登录状态如下
1
2
HttpSession session = request.getSession();
session.setAttribute("isLogin", true);
用户再次访问时,tomcat在会话对象中查看登录状态
1
2
HttpSession session = request.getSession();
session.getAttribute("isLogin");
1.2.2 处理过程
实现了登录状态的浏览器请求服务器模型如下图描述
1.3 多系统情况下登录实现。
1.3.1 同域名下session共享的方式。
利用cookie的特性和session共享实现单点登录。但是无法实现跨域的情况。
sso登录后,可以将Cookie的域设置为顶域,例如.a.com,这样所有子域的系统都可以访问到顶域的Cookie。我们在设置Cookie时,只能设置顶域和自己的域,不能设置其他的域。比如:我们不能在自己的系统中给baidu.com的域设置Cookie。这样所有的子系统都会带上该cookie。
多服务器的Sesssion共享实现cookie session的统一验证。
在sso系统登录了,这时再访问app1,Cookie也带到了app1的服务端(Server),app1的服务端把系统的Session共享。共享Session的解决方案有很多,例如:Spring-Session。
1.3.2 CAS实现单点登录。CentralAuthentication Service
CAS是Central
Authentication Service的简称,统一认证中心。CAS认证协议定义了统一认证的交互过程,同时CAS发行了3个版本。
V1实现单点登录。
V2增加代理模式,代理模式是一种更复杂形式的认证,即认证的Web应用(CAS Client)可以作为代理直接访问需要认证的后端服务(如邮件服务器),浏览器用户无需再和后端服务直接进行认证交互。
v3版本则更加丰富了协议,认证中心验证票据后不仅可以返回身份ID,还可携带用户昵称、性别等其他用户基本信息。
单点登录全称Single Sign On(以下简称SSO),是指在多系统应用群中登录一个系统,便可在其他所有系统中得到授权而无需再次登录,包括单点登录与单点注销两部分。
相比于单系统登录,sso需要一个独立的认证中心,只有认证中心能接受用户的用户名密码等安全信息,其他系统不提供登录入口,只接受认证中心的间接授权。间接授权通过令牌实现,sso认证中心验证用户的用户名密码没问题,创建授权令牌,在接下来的跳转过程中,授权令牌作为参数发送给各个子系统,子系统拿到令牌,即得到了授权,可以借此创建局部会话,局部会话登录方式与单系统的登录方式相同。这个过程,也就是单点登录的原理。
[if !supportLists]1) [endif]登录过程
相比于单系统登录,sso需要一个独立的认证中心,只有认证中心能接受用户的用户名密码等安全信息,其他系统不提供登录入口,只接受认证中心的间接授权。间接授权通过令牌实现,sso认证中心验证用户的用户名密码没问题,创建授权令牌,在接下来的跳转过程中,授权令牌作为参数发送给各个子系统,子系统拿到令牌,即得到了授权,可以借此创建局部会话,局部会话登录方式与单系统的登录方式相同。这个过程,也就是单点登录的原理,用下图说明。
下面对上图简要描述
[if !supportLists]1) [endif]用户访问系统1的受保护资源,系统1发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数
[if !supportLists]2) [endif]sso认证中心发现用户未登录,将用户引导至登录页面
[if !supportLists]3) [endif]用户输入用户名密码提交登录申请
[if !supportLists]4) [endif]sso认证中心校验用户信息,创建用户与sso认证中心之间的会话,称为全局会话,同时创建授权令牌
[if !supportLists]5) [endif]sso认证中心带着令牌跳转会最初的请求地址(系统1)
[if !supportLists]6) [endif]系统1拿到令牌,去sso认证中心校验令牌是否有效
[if !supportLists]7) [endif]sso认证中心校验令牌,返回有效,注册系统1
[if !supportLists]8) [endif]系统1使用该令牌创建与用户的会话,称为局部会话,返回受保护资源
[if !supportLists]9) [endif]用户访问系统2的受保护资源
[if !supportLists]10)[endif]系统2发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数
[if !supportLists]11)[endif]sso认证中心发现用户已登录,跳转回系统2的地址,并附上令牌
[if !supportLists]12)[endif]系统2拿到令牌,去sso认证中心校验令牌是否有效
[if !supportLists]13)[endif]sso认证中心校验令牌,返回有效,注册系统2
[if !supportLists]14)[endif]系统2使用该令牌创建与用户的局部会话,返回受保护资源
用户登录成功之后,会与sso认证中心及各个子系统建立会话,用户与sso认证中心建立的会话称为全局会话,用户与各个子系统建立的会话称为局部会话,局部会话建立之后,用户访问子系统受保护资源将不再通过sso认证中心,全局会话与局部会话有如下约束关系
[if !supportLists]1) [endif]局部会话存在,全局会话一定存在
[if !supportLists]2) [endif]全局会话存在,局部会话不一定存在
[if !supportLists]3) [endif]全局会话销毁,局部会话必须销毁
问题点:
SSO系统登录后,跳回原业务系统时,带了个参数ST,业务系统还要拿ST再次访问SSO进行验证,觉得这个步骤有点多余。他想SSO登录认证通过后,通过回调地址将用户信息返回给原业务系统,原业务系统直接设置登录状态,这样流程简单,也完成了登录,不是很好吗?
如果在SSO没有登录,而是直接在浏览器中敲入回调的地址,并带上伪造的用户信息,业务系统也认为登录,会产生安全问题。
[if !supportLists]2) [endif]注销过程
单点登录自然也要单点注销,在一个子系统中注销,所有子系统的会话都将被销毁,用下面的图来说明
sso认证中心一直监听全局会话的状态,一旦全局会话销毁,监听器将通知所有注册系统执行注销操作。
下面对上图简要说明
[if !supportLists]1) [endif]用户向系统1发起注销请求
[if !supportLists]2) [endif]系统1根据用户与系统1建立的会话id拿到令牌,向sso认证中心发起注销请求
[if !supportLists]3) [endif]sso认证中心校验令牌有效,销毁全局会话,同时取出所有用此令牌注册的系统地址
[if !supportLists]4) [endif]sso认证中心向所有注册系统发起注销请求
[if !supportLists]5) [endif]各注册系统接收sso认证中心的注销请求,销毁局部会话
[if !supportLists]6) [endif]sso认证中心引导用户至登录页面
1.3.3 Shiro实现会话管理和CAS验证
Shiro中同时已经存在了 cas 认证中心,shiro 官方在 1.2 中就表明已经弃用了CasFilter ,建议使用 buji-pac4j ,使用pac4j 来做单点登录的控制,贷款服务目前使用的CasFilter。
1)登录filter, AccessControlFilter
2)登录授权,验证ticket,CasFilter 。
3)登录信息存储,使用redis统一存储登录信息,key是ticket,value是shiro session。
RedisSessionDAO redisSessionDAO =new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
DefaultWebSessionManager sessionManager =new DefaultWebSessionManager();
//
设置自定义cookiesessionManager.setSessionIdCookie(simpleCookie());
sessionManager.setSessionDAO(redisSessionDAO);
2 贷款服务单点登录应用
2.1 全局会话TGT
所有的系统应用都会引导到CAS Server认证中心去登录。登录成功后,认证中心会产生一个票据叫TGT(Ticket Granting Ticket),TGT即代表了用户与认证中心直接的全局会话。TGT存在,表明该用户处于登录状态。
TGT并没有放在Session中,也就是说,CAS全局会话的实现并没有直接使用Session机制,而是利用了Cookie自己实现的,这个Cookie叫做TGC(Ticket Granting Cookie),它存放了TGT的id,认证中心服务端实现了TGT。
请求的时候带上TGC ,验证全局会话。如果删除了cookie TGC,那么需要重新登录。
2.2 局部会话shiro session
局部会话,第一次验证session时,在cookie中设置loan_session。
3 当前问题及分析
3.1 重定向问题。
3.1.1
Cookie取值
Tomcat有默认的sessionID, JSESSIONID。Shiro在不自定义名称的时候,默认session的cookie名称也是,JSESSIONID。导致存在两个重名cookie name。
Shiro的session域 是.loan.jiaoyi.ke.com 而 tomcat的cookie域是 loan.jiaoyi.ke.com。在域不同的情况下,同名cookie是可以共存的。
登录的时候cookie顺序是这样的,shiro可以取到正确的jsessionId。登出重新登录后,顺序相反,导致取到错误的JSESSIONID。
Cookie取值逻辑是按名称,取匹配的第一个。取值逻辑。
private static javax.servlet.http.Cookie
getCookie(HttpServletRequest request, String cookieName) {
javax.servlet.http.Cookie
cookies[] = request.getCookies();
if (cookies !=null){
for (javax.servlet.http.Cookie
cookie : cookies) {
if (cookie.getName().equals(cookieName))
{
return cookie;
}
}
}
return null;
}
3.1.2 重定向循环
当取到错误的sessionid验证时,验证失败。局部回话验证失败,浏览器重定向CAS服务器,CAS验证通过,返回ticket到贷款服务,贷款服务ticket验证通过,重新设计shiro会话session,浏览器重新访问贷款服务,但是session还是取值错误,所以无限循环重定向,导致报错。
4 解决方案
修改shiro的session名称,使得shiro
session取值正确,解决重定向问题。
升级shiro版本,添加配置,解决了线上重定向丢失参数问题,参数丢失的原因还无法确认,在测试环境没法复现问题。sessionManager.setSessionIdUrlRewritingEnabled(false);
@Bean
public SimpleCookie simpleCookie(){
SimpleCookie simpleCookie =new SimpleCookie();
simpleCookie.setName("loan_session");
simpleCookie.setHttpOnly(true);
return simpleCookie;
}
DefaultWebSessionManager sessionManager =new DefaultWebSessionManager();
//
设置自定义cookiesessionManager.setSessionIdCookie(simpleCookie());
sessionManager.setSessionIdUrlRewritingEnabled(false);
5 分析工具
5.1 远程调试
使用远程调试可以清晰的分析整个登录的处理和验证过程。
在jar包启动脚本中也配置,
jdwp=transport=dt_socket,server=y,suspend=n,address=38115
5.2 抓包工具fidder
使用fidder抓包,分析浏览器的请求过程。
[if !supportLists]6 [endif]引用
单点登录原理及简单实现。https://www.cnblogs.com/scode2/p/8671073.html
Tomcat
session 产生。https://www.cnblogs.com/chenpi/p/5434537.html
SecureRandom简介,https://www.cnblogs.com/deng-cc/p/8064481.html
Java随机工具,https://www.jianshu.com/p/2f6acd169202
单点登录看这一篇。https://yq.aliyun.com/articles/636281
CAS实现单点登录,
https://blog.csdn.net/javaloveiphone/article/details/52439613