1818IP-服务器技术教程,云服务器评测推荐,服务器系统排错处理,环境搭建,攻击防护等

当前位置:首页 - 运维 - 正文

君子好学,自强不息!

Tomcat 容器的安全认证和鉴权

2022-11-14 | 运维 | 1818ip | 573°c
A+ A-

大量的 Web 应用都有安全相关的需求,正因如此,Servlet 规范建议容器要有满足这些需求的机制和基础设施,所以容器要对以下安全特性予以支持:

  • 身份验证:验证授权用户的用户名和密码
  • 资源访问控制:限制某些资源只允许部分用户访问
  • 数据完整性:能够证明数据在传输过程中未被第三方修改
  • 机密性或数据隐私:传输加密(SSL),确保信息只能被信任用户访问

本文就以上问题,对 Tomcat 容器提供的认证和鉴权的设计与实现,以及内部单点登录的原理进行分析。首发于微信公众号顿悟源码。

1. 授权

容器和 Web 应用采用的是基于角色的权限访问控制方式,其中容器需要实现认证和鉴权的功能,而 Web 应用则要实现授权的功能。

在 Servlet 规范中描述了两种授权方式:声明式安全和编程式安全。声明式安全就是在部署描述符中声明角色、资源访问权限和认证方式。以下代码片段摘自 Tomcat 自带的 Manager 应用的 web.xml:

<security-constraint><!--安全约束-->
<web-resource-collection><!--限制访问的资源集合-->
<web-resource-name>HTMLManagercommands</web-resource-name>
<url-pattern>/html/*</url-pattern>
</web-resource-collection>
<auth-constraint><!--授权可访问此资源集合的角色-->
<role-name>manager-gui</role-name>
</auth-constraint>
</security-constraint>

<login-config><!--配置验证方法-->
<auth-method>BASIC</auth-method>
<realm-name>TomcatManagerApplication</realm-name>
</login-config>

<security-role><!--定义一个安全角色-->
<description>
TherolethatisrequiredtoaccesstheHTMLManagerpages
</description>
<role-name>manager-gui</role-name>
</security-role>

这些安全相关的配置,都会在应用部署时,初始化和设置到 StandardContext 对象中。更多详细的内容可查看规范对部署描述文件的解释,接下来看 Tomcat 怎么设计和实现认证及鉴权。

2. 认证和鉴权的设计

Servlet 规范虽然描述了 Web 应用声明安全约束的机制,但没有定义容器与关联用户和角色信息之间的接口。因此,Tomcat 定义了一个 Realm 接口,用于适配身份验证的各种信息源。整体设计的类图如下:

上图中,包含了各个类的核心方法,关键类或接口的作用如下:

  • Realm – 译为域,域有泛指某种范围的意思,在这个范围内存储着用户名、密码、角色和权限,并且提供身份和权限验证的功能,典型的这个范围可以是某个配置文件或数据库
  • CombinedRealm – 内部包含一个或多个 Realm,按配置顺序执行身份验证,任一 Realm 验证成功,则表示成功验证
  • LockOutRealm – 提供用户锁定机制,防止在一定时间段有过多身份验证失败的尝试
  • Authenticator – 不同身份验证方法的接口,主要有 BASIC、DIGEST、FORM、SSL 这几种标准实现
  • Principal – 对认证主体的抽象,它包含用户身份和权限信息
  • SingleSignOn – 用于支持容器内多应用的单点登录功能

2.1 初始化

Realm 是容器的一个可嵌套组件,可以嵌套在 Engine、Host 和 Context 中,并且子容器可以覆盖父容器配置的 Realm。默认的 server.xml 在 Engine 中配置了一个 LockOutRealm 组合域,内部包含一个 UserDatabaseRealm,它从配置的全局资源 conf/tomcat-users.xml 中提取用户信息。

web.xml 中声明的安全约束会初始化成对应的 SecurityConstraint、SecurityCollection 和 LoginConfig 对象,并关联到一个 StandardContext 对象。

在上图可以看到,AuthenticatorBase 还实现了 Valve 接口,StandardContext 对象在配置的过程中,如果发现声明了标准的验证方法,那么就会把它加入到自己的 Pipeline 中。

3. 一次请求认证和鉴权过程

Context 在 Tomcat 内部就代表着一个 Web 应用,假设配置使用 BASIC 验证方法,那么 Context 内部的 Pipeline 就有 BasicAuthenticator 和 StandardContextValve 两个阀门,当请求进入 Context 管道时,就首先进行认证和鉴权,方法调用如下:

整个过程的核心代码就在 AuthenticatorBase 的 invoke 方法中:

publicvoidinvoke(Requestrequest,Responseresponse)throwsIOException,ServletException{
LoginConfigconfig=this.context.getLoginConfig();
//0.Session对象中是否缓存着一个已经进行身份验证的Principal
if(cache){
Principalprincipal=request.getUserPrincipal();
if(principal==null){
Sessionsession=request.getSessionInternal(false);
if(session!=null){
principal=session.getPrincipal();
if(principal!=null){
request.setAuthType(session.getAuthType());
request.setUserPrincipal(principal);
}
}
}
}
//对于基于表单登录,可能位于安全域之外的特殊情况进行处理
StringcontextPath=this.context.getPath();
StringrequestURI=request.getDecodedRequestURI();
if(requestURI.startsWith(contextPath)&&requestURI.endsWith(Constants.FORM_ACTION)){
return;
}
}
//获取安全域对象,默认配置是LockOutRealm
Realmrealm=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);
}
inti;
//1.检查用户数据的传输安全约束
if(!realm.hasUserDataPermission(request,response,constraints)){
//验证失败
//Authenticator已经设置了适当的HTTP状态代码,因此我们不必做任何特殊的事情
return;
}
//2.检查是否包含授权约束,也就是角色验证
booleanauthRequired=true;
for(i=0;i<constraints.length&&authRequired;i++){
if(!constraints[i].getAuthConstraint()){
authRequired=false;
}elseif(!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 应用,可通过以下配置实现:

<Hostname="localhost"...>
...
<ValveclassName="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,欢迎关注。

本文来源:1818IP

本文地址:https://www.1818ip.com/post/9242.html

免责声明:本文由用户上传,如有侵权请联系删除!

发表评论

必填

选填

选填

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。