Tomcat单点登录配置及源码分析

我们上网的时候,一定遇到过类似这样的情况,例如使用网易邮箱时进行了登录操作,之后再访问网易的博客系统时,发现自动以之前的ID登录了。这种实现在计算机中称为SSO(Single Sign On),即我们常说的单点登录。这种在关联网站间共享认证信息,避免需要在多个系统中重复输入帐户信息的行为,是SSO要解决的。

对于许多应用,可能会独立部署等情况,所以常会采用cas的形式,来实现SSO。

我们今天要了解的,是作为在同一个Tomcat中部署的应用之间,如何实现SSO,避免重复登录。

预备:

首先,有几点预备知识需要先了解一下。

  1. 在Tomcat架构设计中,不同的Container中包含了Peipline。各个Pipeline中可以添加多种不同形式的Valve。例如我们之前提到的AccessLogValveTomcat的AccessLogValve介绍

  2. Tomcat中session的实现,最常用的是Cookie Session, 通过将名为JSESSIONID的cookie写回浏览器,实现session。我们在前面的文章里也描述过。深入Tomcat源码分析Session

  3. 关于认证的一些内容,可以参考介绍过的Basic认证。你可能不了解的Basic认证

环境:

有了这些准备之后,我们开始进行环境的搭建和实验。

以Tomcat自带的几个应用为例,我们启动Tomcat后,访问这两个应用:docs、examples 我们看到,默认是不需要登录的,都可以直接访问。

此时,在docs应用的web.xml中增加如下配置:

    <security-constraint>    
<display-name>Security Constraint</display-name>    
<web-resource-collection>   
 <web-resource-name>Protected Area</web-resource-name> 
   <url-pattern>/*</url-pattern>   
 </web-resource-collection> 
   <auth-constraint>  
  <role-name>tomcat</role-name> 
   </auth-constraint>    </security-constraint>    
<login-config>    <auth-method>BASIC</auth-method>  
  <realm-name>SSO Test</realm-name>    
</login-config>    <security-role>  
  <role-name>tomcat</role-name>   
 </security-role>
复制代码

此时重启Tomcat,再次请求docs应用,发现需要验证了。

同样,再修改examples应用的web.xml,限制对于其直接访问,在文件中增加如下内容: /*。只需要增加这个就可以了,下面是修改内容对应的位置参考。

<web-resource-collection>       
  <web-resource-name>Protected Area - Allow methods</web-resource-name>         <url-pattern>/jsp/security/protected/*</url-pattern>  
           <url-pattern>/*</url-pattern>  
       <http-method>DELETE</http-method>     
    <http-method>GET</http-method>      
   <http-method>POST</http-method>  
       <http-method>PUT</http-method>   
   </web-resource-collection>
复制代码

修改之后,examples也需要登录才能访问了。由于同样的认证,我们对两个应用的访问需要重复输入用户名、密码进行认证,此时,SSO的配置就显出了必要性了。

在Tomcat的server.xml中,默认的Host,localhost中,增加以下Valve:

<Valve className="org.apache.catalina.authenticator.SingleSignOn"/>
复制代码

再次重启Tomcat,这个时候SSO已经生效了,你再重新访问上面两个应用时,只需要对其中一个进行认证即可,是不是很容易?

原理:

在前面分析请求流程的几篇文章中,我们介绍过从CoyoteAdapter进行service处理,再到达各个Pipeline、Valve。(Facade模式与请求处理)

而这些Valve中,对于SSO的Valve SingleSignOn是在认证的ValveAuthenticatorBase之前执行。

在SingleSignOn中,会先进行userPrincipal的判断,不为空就会直接向后执行,为空时,判断请求中是否包含SSO Cookie。

    if (request.getUserPrincipal() != null) {                           getNext().invoke(request, response);     
       return;        }       
 // Check for the single sign on cookie 
       Cookie cookie = null;       
 Cookie cookies[] = request.getCookies();       
 if (cookies != null) {        
    for (int i = 0; i < cookies.length; i++) {       
         if (Constants.SINGLE_SIGN_ON_COOKIE.equals(cookies[i].getName())) {                    cookie = cookies[i];           
         break;                }            }        }       
 if (cookie == null) {        
    getNext().invoke(request, response);         
   return;        }
复制代码

对于第一个就进认证的应用,走的流程基本和配置之前一样,区别就在于SSO配置后,会把认证的信息,添加到Cookie中。并将其存储并和一个ssoId进行关联。

BasicAuthenticator:

对于docs应用,使用的是Basic认证方式

 principal = context.getRealm().authenticate(username, password);   
  if (principal != null) {      
 register(request, response, principal,  HttpServletRequest.BASIC_AUTH, username, password);      
          return (true);   
     } }
复制代码

FormAuthenticator

对于examples应用,使用的是Form的认证方式,如果是Form认证的应用不是第一个请求,则在请求到达时,已经进行过认证,后面的请求会直接获取session并关联到ssoId上。 如果是初次请求即访问Form认证的应用,SsoId还没值,流程基本和Basic一样,不同的是从表单中提取用户名和密码信息,再进行register

Principal principal = request.getUserPrincipal();  
 String ssoId = (String) request.getNote(Constants.REQ_SSOID_NOTE);  
 if (principal != null) {    
// Associate the session with any existing SSO session  
   if (ssoId != null) {      
associate(ssoId, request.getSessionInternal(true)); // 注意这里,把新获取到的sessionId关联到ssoId中 
 } 
 return true; }

这里register会把认证的信息添加, 在ssoId为空时,进行Cookie的创建,

        String ssoId = (String) request.getNote(Constants.REQ_SSOID_NOTE);        if (ssoId == null) {           
 ssoId = sessionIdGenerator.generateSessionId();      
      Cookie cookie = new Cookie(Constants.SINGLE_SIGN_ON_COOKIE, ssoId);            cookie.setMaxAge(-1); 
           cookie.setPath("/");  
          // Bugzilla 41217      
      cookie.setSecure(request.isSecure());
            // Bugzilla 34724           
 String ssoDomain = sso.getCookieDomain(); 
           if(ssoDomain != null) {                cookie.setDomain(ssoDomain);         
   }    
        // Configure httpOnly on SSO cookie using same rules as session cookies           
 if (request.getServletContext().getSessionCookieConfig().isHttpOnly() ||                    request.getContext().getUseHttpOnly()) {      
          cookie.setHttpOnly(true);          
  }            
response.addCookie(cookie);        
    // Register this principal with our SSO valve            sso.register(ssoId, principal, authType, username, password);            request.setNote(Constants.REQ_SSOID_NOTE, ssoId);
复制代码

Cookie不为空时,进行ssoId和session的关联

protected boolean associate(String ssoId, Session session) {        SingleSignOnEntry sso = cache.get(ssoId);  
      if (sso == null) {          
  if (containerLog.isDebugEnabled()) {   
             containerLog.debug(sm.getString("singleSignOn.debug.associateFail",                        ssoId, session));           
 }            return false;    
    } else {            }        
    sso.addSession(this, ssoId, session);      
      return true;        }    }
复制代码

我们注意到这行代码sso.addSession(this, ssoId, session) 这里会给session添加一个listener,这个listener会在session过期销毁时,把sso的session也移除掉

应用的SSO

在Pipeline中从SingleSignOn这个Valve开始,一直调用到AuthenticatorBase,再到达其实现类. SingleSignOn这个Valve处理请求时,判断entry是否为空,此时由于前面的应用已经存储过该信息,所以这里不为空,就会据此设置request中的authType和principal

  SingleSignOnEntry entry = cache.get(cookie.getValue()); 
   if (entry != null) {  
 request.setNote(Constants.REQ_SSOID_NOTE, cookie.getValue()); 
  // Only set security elements if reauthentication is not required    if (!getRequireReauthentication()) {         request.setAuthType(entry.getAuthType());     request.setUserPrincipal(entry.getPrincipal());   
         }
复制代码

而后面的Valve中,认证时首先会判断principal是否为空。由于前置的sso已经把这些信息填充过了,所以这里就会走这样的逻辑:

    public void invoke(Request request, Response response)        throws IOException, ServletException {   
     // Have we got a cached authenticated Principal to record?        if (cache) {          
  Principal principal = request.getUserPrincipal(); // 这里不为空            if (principal == null) {        
        Session session = request.getSessionInternal(false);                if (session != null) {                
    principal = session.getPrincipal();   
                 if (principal != null) {                        request.setAuthType(session.getAuthType());                        request.setUserPrincipal(principal);                
    }                }            }        }
复制代码

总结一下:

单点登录的实现,是在第一次进行认证的时候,将认证信息进行存储。后续相同域的请求到达时,会先判断是否存储了单点登录的认证信息,如果已经存储过,就将其添加到新到达的request中,以此进行后续的认证,从而实现SSO.


作者:用户3438396967893
链接:https://juejin.cn/post/7091976122593706015
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值