大量的 Web 应用都有安全相关的需求,正因如此,Servlet 规范建议容器要有满足这些需求的机制和基础设施,所以容器要对以下安全特性予以支持:
- 身份验证:验证授权用户的用户名和密码
- 资源访问控制:限制某些资源只允许部分用户访问
- 数据完整性:能够证明数据在传输过程中未被第三方修改
- 机密性或数据隐私:传输加密(SSL),确保信息只能被信任用户访问
本文就以上问题,对 Tomcat 容器提供的认证和鉴权的设计与实现,以及内部单点登录的原理进行分析。首发于微信公众号顿悟源码.
1. 授权
容器和 Web 应用采用的是基于角色的权限访问控制方式,其中容器需要实现认证和鉴权的功能,而 Web 应用则要实现授权的功能。
在 Servlet 规范中描述了两种授权方式:声明式安全和编程式安全。声明式安全就是在部署描述符中声明角色、资源访问权限和认证方式。以下代码片段摘自 Tomcat 自带的 Manager 应用的 web.xml:
<security-constraint> <!-- 安全约束 -->
<web-resource-collection> <!-- 限制访问的资源集合 -->
<web-resource-name>HTML Manager commands</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>Tomcat Manager Application</realm-name>
</login-config>
<security-role><!-- 定义一个安全角色 -->
<description>
The role that is required to access the HTML Manager pages
</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 方法中:
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 name="localhost" ...>
...
<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,欢迎关注。
Tomcat 容器的安全认证和鉴权的更多相关文章
-
spring boot / cloud (十四) 微服务间远程服务调用的认证和鉴权的思考和设计,以及restFul风格的url匹配拦截方法
spring boot / cloud (十四) 微服务间远程服务调用的认证和鉴权的思考和设计,以及restFul风格的url匹配拦截方法 前言 本篇接着<spring boot / cloud ...
-
深入理解k8s中的访问控制(认证、鉴权、审计)流程
Kubernetes自身并没有用户管理能力,无法像操作Pod一样,通过API的方式创建/删除一个用户实例,也无法在etcd中找到用户对应的存储对象. 在Kubernetes的访问控制流程中,用户模型是 ...
-
shiro,基于springboot,基于前后端分离,从登录认证到鉴权,从入门到放弃
这个demo是基于springboot项目的. 名词介绍: ShiroShiro 主要分为 安全认证 和 接口授权 两个部分,其中的核心组件为 Subject. SecurityManager. Re ...
-
web系统认证与鉴权中的一些问题
认证鉴权系统的初心: 空间管理: 1.他是谁? 他登陆了没有? 2.他要做什么? 2.1 他要使用什么功能? 他是否有这个功能的权限. 2.2 他要使用这个功能做什么操作? 他是否有这个功能的这个操作 ...
-
白话OAuth2用户认证及鉴权标准流程
一.OAuth2需求场景 在说明OAuth2需求及使用场景之前,需要先介绍一下OAuth2授权流程中的各种角色: 资源拥有者(User) - 指应用的用户 认证服务器 (Authorization S ...
-
Java架构笔记:用JWT对SpringCloud进行认证和鉴权
写在前面 喜欢的朋友可以关注下专栏:Java架构技术进阶.里面有大量batj面试题集锦,还有各种技术分享,如有好文章也欢迎投稿哦. image.png JWT(JSON WEB TOKEN)是基于RF ...
-
【Python】Django用户、认证、鉴权模块使用
此文是总结Django官方网站里面的Document的文章 User authentication in Django http://www.djangoproject.com/documentati ...
-
【Spring Cloud &; Alibaba 实战 | 总结篇】Spring Cloud Gateway + Spring Security OAuth2 + JWT 实现微服务统一认证授权和鉴权
一. 前言 hi,大家好~ 好久没更文了,期间主要致力于项目的功能升级和问题修复中,经过一年时间的打磨,[有来]终于迎来v2.0版本,相较于v1.x版本主要完善了OAuth2认证授权.鉴权的逻辑,结合 ...
-
认证鉴权与API权限控制在微服务架构中的设计与实现(四)
引言: 本文系<认证鉴权与API权限控制在微服务架构中的设计与实现>系列的完结篇,前面三篇已经将认证鉴权与API权限控制的流程和主要细节讲解完.本文比较长,对这个系列进行收尾,主要内容包括 ...
随机推荐
-
grunt配置太复杂?发布一个前端构建工具,简单高效,自动跳过未更新的文件
做前端项目,如果没有一个自动化构建工具,手动处理那简直就是坑爹O(∩_∩)O.于是上网了解了下,grunt用的人不少,功能也挺强大.看了一下grunt的配置(包括gulp),感觉稍显复杂.当时项目结构 ...
-
《Java程序设计》第五次实验实验报告
实验封面 一.实验内容 1.阅读理解源码进入07_httpd所在的目录,使用vi编辑器理解源代码. 2.编译应用程序使用gcc编译器,分别对文件夹下的copy.c和httpd.c进行编译,出现copy ...
-
MFC原创:三层架构01(人事管理系统)DAL
VC++/MFC Window编程原创教程文件夹 C++课程设计来着.但还没学过数据,也还没理解过三层架构,就把这个作业深化点来做了.尽管要做的这个人事管理系统看起来是挺简单的,无非就是处理员工信息. ...
-
让浏览器支持 jquery ajax load 前进、后退 功能
BEGIN; 一般在做 ajax load 的时候,非常多人都不会考虑到须要浏览器支持前进后退功能,由于大部分人都不知道能够实现. 近期遇到这个问题,经过一小段研究,发现github已经有现成的开源工 ...
-
Xshell无法连接到LINUX虚拟机
首先与遇到的情况是,在虚拟机下安装了Linux后,xshell无法连接远程的虚拟机. 我遇到的情况是虚拟机可以ping 主机,主机确ping不了虚拟机. 使用的VM设置了两个网卡,一个nat 一个h ...
-
Android下DrawerLayout的使用
Android下DrawerLayout的使用 DrawerLayout见名知意,就是一个具有抽屉效果的布局,看看这个效果图,是不是感觉很炫酷 这么炫的效果其实不一定非要用类似一些SlidingMen ...
-
使用windows脚本移动文件
1. 移动脚本 在部署web项目时,一般需要将打包的war包发布到Tomcat目录下,所以自己就在网上查找资料写了一个简略的移动文件的脚本,如下: @echo off echo "使用bat ...
-
px、pt、ppi、dpi、dp、sp之间的关系
http://www.woshipm.com/pmd/176328.html 各自的定义: px:pixel,像素,电子屏幕上组成一幅图画或照片的最基本单元 pt: point,点,印刷行业常用单位, ...
-
Mysql数据库的mysql Schema 究竟有哪些东西&; 手工注入的基础要领
#查看数据库版本号 mysql> select @@version; +------------+ | @@version | +------------+ | 5.5.16-log | +- ...
-
python迭代器的内置函数
1.迭代器: 内置函数: (1)iter() -__iter__() (2)next() -__next__() 2.迭代器的举例; 对于Fibs数列,我们对其进行 限量输出: 实现代码如下: cla ...