一、领域(Realm):
1、Principal接口代表角色信息,包含了三个成员:用户名、密码、role列表(以逗号分隔),对应了tomcat-users.xml文件中一行user信息:
GenericPrincipal作为Principal接口的默认实现类,提供了hasRole函数,通过这个函数可以判断该角色是否支持指定的role;
2、Realm接口代表领域对象,是一个用来对用户进行身份验证的组件;可以认为Realm是Principal的管理器,包含了用户角色的集合;每个Realm对象都会与一个context容器对象相关联;hasRole方法判断指定的用户角色是否支持指定的role,authenticate用来判断进行用户身份验证,该方法重载了四个实现方法,其中最重要和最常用的是用户名和密码验证
3、Realm接口的基本实现形式是RealmBase类,这是一个抽象类,提供了具体领域实现类的相同操作部分,不同部分由具体子类自己去实现,默认情况下会使用MemoryRealm类的实例作为验证用的领域对象;
4、当第一次调用MemoryRealm实例时,它会读取tomcat-users.xml文件的内容,创建Digester对象读取:
可以看到读取配置文件时使用了MemoryRuleset规则对象,在MemoryRuleset对象读取时:
可以看到该规则会读取tomcat-users/下的username, password, roles到一个GenericPrincipal对象中并添加到领域对象Realm中;
二、安全验证机制:
- loginConfig是登录配置类,loginConfig实例封装了领域对象的名字和身份验证方法,身份验证方法有:BASIC, FORM, DIGEST和CLIENT-CERT;如果身份验证方法是FORM,还需要指定loginpage和errorPage属性;
- loginConfig实例会读取web项目的WEB-INF/web.xml文件里面的login-config配置,比如在web.xml项目里添加如下配置,则在浏览器中访问该项目时会弹出身份验证的对话框:
<security-constraint> <web-resource-collection> <web-resource-name>MySecurityTest</web-resource-name> <url-pattern>/*</url-pattern> </web-resource-collection> <auth-constraint> <role-name>admin-gui</role-name> <role-name>manager-gui</role-name> </auth-constraint> </security-constraint> <login-config> <auth-method>BASIC</auth-method> <realm-name>MySecurityTest</realm-name> </login-config>
1) web-resource-collection:
此元素确定应该保护的资源,所有security-constraint元素都必须包含至少一个web-resource-collection项.此元素由一个给出任意标识名称的web-resource-name元素、一个确定应该保护URL 的url-pattern元素、一个指出此保护所适用的HTTP命令(GET、POST等,缺省为所有方法)的http-method元素和一个提供资料的可选description元素组成。
2) auth-constraint:
指出哪些用户应该具有受保护资源的访问权。此元素应该包含一个或多个标识具有访问权限的用户类别role-name元素,以及包含(可选)一个描述角色的description元素。
3) user-data-constraint:
这是个可选的元素,指出在访问相关资源时使用任何传输层保护。它必须包含一个transport-guarantee子元素(合法值为NONE、INTEGRAL或CONFIDENTIAL),并且可选地包含一个description元素。transport-guarantee为NONE值将对所用的通讯协议不加限制。INTEGRAL值表示数据必须以一种防止截取它的人阅读它的方式传送。虽然原理上(并且在未来的HTTP版本中),在INTEGRAL和CONFIDENTIAL之间可能会有差别,但在当前实践中,他们都只是简单地要求用SSL。
4) login-config:
auth-method指出了认证的方法,一共有四种:BASIC,FORM,DIGEST,CLIENT-CERT,realm-name表示域名,对于FORM的认证方式还需要指定loginpage和errorpage,对于下面的form表单:
<form name="loginform" method="post" action="j_security_check"> <INPUT name="j_username" type="text"> <INPUT name="j_password" TYPE="password"> <input type="submit" value="登 录" > </form>
form 的action 必须是j_security_check, method="post", 用户名 name="j_username" , 密码name="j_password" 这些都是固定的元素;
- loginConfig类对应上面xml配置文件里面的login-config项,SecurityCollection类对应配置文件里面的web-resource-collection,SecurityConstraint对应配置文件里面的security-constraint(web-resource-collection配置是作为security-constraint的子配置项存在的),将配置文件解析到对应的对象里面是通过Digester对象解析,解析时的规则类是WebRuleSet类;
- Authenticator是代表验证的接口,这个只是一个空的接口,起到一个标识的作用,AuthenticatorBase实现了这个接口,实现了不同验证方式的公共代码,不同的验证方式均是从这个类派生出子类来实现的;这个类同时也是一个阀(valve),他会安装到context容器的pipeline列表中,这样在访问web项目时会调用pipeline.invoke,也就是会调用验证阀进行身份验证;
- 安全认证访问流程:
1) standardcontext.start():调用lifecycle.fireLifecycleEvent(START_EVENT, null);发送START_EVENT事件;
2) ContextConfig接收到start事件后调用start方法;
3) Start方法中调用ContextConfig.authenticatorConfig方法,加载BasicAuthenticator对象并添加到context.pipeline.valves[]中;
4) 当有连接请求到达时,会调用HttpProcessor.parseHeaders方法,里面header里面有authorization字段,则设置request.authorization标记为true;
5)Context.invoke方法会调用pipeline.invoke,然后会调用StandardPipelineValveContext.invokeNext方法,在这里面会调用到验证阀AuthenticatorBase.invoke方法,其中代码段如下:
这里的authenticate方法会调用不同验证子类的authenticate方法来验证;
- BASIC验证:
BASIC的验证方式相对不是很安全,验证字符串以“Authorization: Basic username:password”的形式发送,其中username:password以base64编码的形式存在,如果被截获后将这个字符串进行base64解码,即可查看到用户名和密码明文(如果请求是在安全的SSL层上,则BASIC验证方式依然是安全的);
认证规则如下:
1) 客户端访问受保护的资源;
2) 服务器返回401 Unauthorized状态,响应头信息如下图所示,其中WWW-Authenticate:Basic realm="MyRealm"表示该资源的受保护信息。
3) 浏览器根据响应弹出窗口,提示用户输入用户名和密码。
4) 浏览器将客户端将输入的用户名、密码用Base64算法进行加密后发送给服务器。例如,使用用户名、密码都是“java”进行登录,浏览器则发送的请求头中包含“Authorization: Basic amF2YTpqYXZh”,其中“amF2YTpqYXZh”是用户名、密码组成的字符串“java:java”进行Base64加密得到的结果。
5) 如果认证成功,则返回相应的受保护资源。如果认证失败,则仍返回401 Unauthorized状态,要求重新进行认证。
- DIGEST验证:
Digest认证提供了一种不使用明文发送用户名密码的方式。Digest认证的规则如下:
1) 客户端访问受保护的资源。
2) 服务器返回401 Unauthorized状态,响应头信息如下图所示,其中
WWW-Authenticate:Digest realm="MyRealm", qop="auth", nonce="1454307975468:a0aefce3e84d69723e6f04fda5674ad0", opaque="23BB4CB60BFE2CD08B490A16B86C9661"
表示相关的安全域信息、随机数信息(nonce)等。
3) 浏览器根据响应弹出窗口,提示用户输入用户名和密码。
4) 浏览器将客户端将输入的用户名以明文的方式、密码等其他信息以摘要的方式返回给服务端。
5) 服务端将用户名、正确的密码等信息按规则进行摘要加密,与客户端提供的信息进行比对。如果认证成功,则返回相应的受保护资源。如果认证失败,则仍返回401 Unauthorized状态,要求重新进行认证。
- FORM验证:
Basic和Digest认证由于其自身的设计,各浏览器的实现都是弹出一个无所谓美观的对话框,对用户体验有很大的影响。Form认证中定义了采集用户信息的登录页面、登录失败页面,通过用户自定义实现这两个页面,能够完成美观的登录操作。在web.xml中配置Form认证方式及登录页面示例如下:
<login-config> <auth-method>FORM</auth-method> <realm-name>file</realm-name> <form-login-config> <form-login-page>/login.xhtml</form-login-page> <form-error-page>/error.xhtml</form-error-page> </form-login-config> </login-config>
在Servlet规范中规定,使用Form认证时,表单提交的action必须为j_security_check,而获取登录信息的字段必须为j_username和j_password。
认证流程如下:
1) 查看是否已经对当前用户进行了认证,避免重复认证造成资源浪费:checkForCachedAuthentication(request, response, true)。
2) 如果没有认证,则需要保存当前用户需要保存的页面:saveRequest(request, session),然后跳转到登录页面:forwardToLoginPage(request, response, config)。
3) 用户提交了用户名和密码,进行认证工作:principal = realm.authenticate(username, password);如果认证失败,则跳转至失败页面:forwardToErrorPage(request, response, config);如果认证成功,则跳转至第二步保存的页面:response.sendRedirect(response.encodeRedirectURL(uri))。
4) 浏览器接收到302重定向状态码后,将页面跳转至最初访问的页面。
5) 再次走进Form认证器的认证流程,通过判断条件matchRequest(request)将认证主体(Principal)保存在Request和Session中,判断条件为:已经通过了认证;存在一个已保存的页面且与当前请求页面路径相同。然后将本次请求的所有信息都重置为最初的请求信息:restoreRequest(request, session)。此后的访问在第一步即直接返回了。
- CLIENT-CERT验证:
Client认证依赖于HTTPS,因此是Java EE安全规范中安全性最高的一种认证方式。使用Client认证需要在web.xml中配置如下:
<login-config>
<auth-method>CLIENT-CERT</auth-method>
</login-config>