整个过程的核心代码就在 AuthenticatorBase 的 invoke 方法中:
public void invoke(Request request, Response response) throws IOException, ServletException { LoginConfig config = this.context.getLoginConfig(); // 0. Session 对象中是否缓存着一个已经进行身份验证的 Principal 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); } } } } // 对于基于表单登录,可能位于安全域之外的特殊情况进行处理 String contextPath = this.context.getPath(); String requestURI = request.getDecodedRequestURI(); if (requestURI.startsWith(contextPath) && requestURI.endsWith(Constants.FORM_ACTION)) { return; } } // 获取安全域对象,默认配置是 LockOutRealm Realm realm = this.context.getRealm(); // 根据请求 URI 尝试获取配置的安全约束 SecurityConstraint [] constraints = realm.findSecurityConstraints(request, this.context); if ((constraints == null) /* && (!Constants.FORM_METHOD.equals(config.getAuthMethod())) */ ) { // 为 null 表示访问的资源没有安全约束,直接访问下一个阀门 getNext().invoke(request, response); return; } // 确保受约束的资源不会被 Web 代理或浏览器缓存,因为缓存可能会造成安全漏洞 if (disableProxyCaching && !"POST".equalsIgnoreCase(request.getMethod())) { if (securePagesWithPragma) { response.setHeader("Pragma", "No-cache"); response.setHeader("Cache-Control", "no-cache"); } else { response.setHeader("Cache-Control", "private"); } response.setHeader("Expires", DATE_ONE); } int i; // 1. 检查用户数据的传输安全约束 if (!realm.hasUserDataPermission(request, response, constraints)) { // 验证失败 // Authenticator已经设置了适当的HTTP状态代码,因此我们不必做任何特殊的事情 return; } // 2. 检查是否包含授权约束,也就是角色验证 boolean authRequired = true; for(i=0; i < constraints.length && authRequired; i++) { if(!constraints[i].getAuthConstraint()) { authRequired = false; } else if(!constraints[i].getAllRoles()) { String [] roles = constraints[i].findAuthRoles(); if(roles == null || roles.length == 0) { authRequired = false; } } } // 3. 验证用户名和密码 if(authRequired) { // authenticate 是一个抽象方法,由不同的验证方法实现 if (!authenticate(request, response, config)) { return; } } // 4. 验证用户是否包含授权的角色 if (!realm.hasResourcePermission(request, response,constraints,this.context)) { return; } // 5. 已满足任何和所有指定的约束 getNext().invoke(request, response); }另外,AuthenticatorBase 还有一个比较重要的 register() 方法,它会把认证后生成的 Principal 对象设置到当前 Session 中,如果配置了SingleSignOn 单点登录的阀门,同时把用户身份、权限信息关联到 SSO 中。
4. 单点登录Tomcat 支持通过一次验证就能访问部署在同一个虚拟主机上的所有 Web 应用,可通过以下配置实现:
<Host ...> ... <Valve className="org.apache.catalina.authenticator.SingleSignOn"/> ... </Host>Tomcat 的单点登录是利用 Cookie 实现的:
当任一 Web 应用身份验证成功后,都会把用户身份信息缓存到 SSO 中,并生成一个名为 JSESSIONIDSSO 的 Cookie
当用户再次访问这个主机时,会通过 Cookie 拿出存储的用户 token,获取用户 Principal 并关联到 Request 对象中
在单机环境下,没有问题,在集群环境下,Tomcat 支持 Session 的复制,那单点登录相关的信息也会同步复制吗?后续会继续分析 Tomcat 集群的原理和实现。
5. 小结本文介绍的是 Tomcat 内部实现的登录认证和权限,而应用程序通常都是通过 Filter 或者自定义的拦截器(如 Spring 的 Interceptor)实现登录,或者使用第三方安全框架,比如 Shiro,但是原理都差不多。
至此,除了集群的实现,Tomcat 的核心原理已经分析完毕,接下来将会模拟实现一个简单的 Tomcat,欢迎关注。