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后,访问这两个应用:docsexamples 我们看到,默认是不需要登录的,都可以直接访问。

此时,在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,限制对于其直接访问,在文件中增加如下内容: <url-pattern>/*</url-pattern>。只需要增加这个就可以了,下面是修改内容对应的位置参考。

<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.

PS. 微信公众号里,代码的罗列真心不好弄,通过其他的Markdown编辑器预览效果很不错的,粘过来就变了形了。各位如果有好的工具或办法,欢迎留言或私信,谢谢。

  1. 相关阅读

    1.  深入Tomcat源码分析Session到底是个啥!

    2. 对于过期的session,Tomcat做了什么?

    3. 禁用Cookie后,Session怎么样使用?

    4. Tomcat的AccessLogValve介绍

    5. 详解集群内Session高可用的实现原理

    6. 快看Apache那个二道贩子

    猜你喜欢

    1. 深度揭秘乱码问题背后的原因及解决方式

    2. WEB应用是怎么被部署的?

    3. 怎样调试Tomcat源码

    4. IDE里的Tomcat是这样工作的!

    5. 重定向与转发的本质区别

    6. 怎样阅读源代码

扫描或长按下方二维码,即可关注!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值