概述
Jetty的强大之处在于可以自由的配置某些组建的存在与否,以提升性能,减少复杂度,而其本身也因为这种特性而具有很强的可扩展性。SecurityHandler就是Jetty对Servlet中Security框架部分的实现,并可以根据实际需要装卸和替换。Servlet的安全框架主要有两个部分:数据传输的安全以及数据授权,对数据传输的安全,可以使用SSL对应的Connector实现,而对于数据授权安全,Servlet定义了一套自己的框架。
Servlet的安全框架支持两种方式的验证:首先,是用于登陆的验证,对于定义了role-name的资源都需要进行登陆验证,Servlet支持NONE、BASIC、CLIENT-CERT、DIGEST、FORM等5种验证方式(<login-config>/<auth-method>);除了用户登陆验证,Servlet框架还定义了role的概念,一个role可以包含一个或多个用户,一个用户可以隶属于多个role,一个资源可以有一个或多个role,只有这些定义的role才能访问该资源,用户只能访问它所隶属的role能访问的资源。另外,对一个Servlet来说,还可以定义role-name到role-link的映射关系,从文档上,这里的role-name是Servlet中使用的名字,而role-link是Container中使用的名字,感觉很模糊,从Jetty的角度,role-name是web.xml中在<security-constraint>/<auth-constraint>/<role-name>中对一个URL Pattern的role定义,而role-link则是UserIdentity中roles数组的值,而UserIdentity是LoginService中创建的,它从文件、数据库等加载已定义的user的信息:用户名、密码、它隶属的role等,如果Servlet中没有定义role-name到role-link的映射,则直接使用role-name去UserIdentity中比较role信息。
关于Servlet对Security框架的具体解释,可以参考Oracle的文档:http://docs.oracle.com/cd/E19798-01/821-1841/6nmq2cpk7/index.html
在web.xml中,对用于登陆验证方式的定义如下:
<login-config>
<auth-method>FORM</auth-method>
<realm-name>Example-Based Authentiation Area</realm-name>
<form-login-config>
<form-login-page>/jsp/security/protected/login.jsp</form-login-page>
<form-error-page>/jsp/security/protected/error.jsp</form-error-page>
</form-login-config>
</login-config>
OR
<login-config>
<auth-method>BASIC</auth-method>
<realm-name>Tomcat Manager Application</realm-name>
</login-config>
而对资源所属role的定义如下:
<security-constraint>
<security-constraint>
<web-resource-collection>
<web-resource-name>Status interface</web-resource-name>
<url-pattern>/status/*</url-pattern>
</web-resource-collection>
...
<auth-constraint>
<role-name>manager-gui</role-name>
<role-name>manager-script</role-name>
<role-name>manager-jmx</role-name>
<role-name>manager-status</role-name>
</auth-constraint>
</security-constraint>
Jetty对Servlet Security实现概述和类图
在Jetty中,使用Authenticator接口抽象不同用户登陆验证的逻辑;使用LoginService接口抽象对用户名、密码的验证;使用UserIdentity保存内部定义的一个用户的用户名、密码、role集合;使用ConstraintMapping保存URL Pattern到role集合的映射;使用UserIdentity.Scope保存一个Servlet中role-name到role-link的映射。他们的类图如下:
UserIdentity实现
UserIdentity表示一个用户的认证信息,它包含Subject和UserPrincipal,其中Subject是Java Security框架定义的类型,而UserPrincipal则用于存储用户名以及认证信息,在Jetty中一般使用KnownUser来存储,它包含了UserName以及Credential实例,其中Credential可以是Crypt、MD5、Password等。在Credential中定义了check方法用于验证传入的credential是否是正确的。
IdentityService实现
IdentityService我猜原本用于将UserIdentity、RunAsToken和当前Thread关联在一起,以及创建UserIdentity、RunAsToken,然而我看的版本中,DefaultIdentityService貌似还没有实现完成,目前只是根据提供的Subject、Principal、roles创建DefaultUserIdentity实例,以及使用runAsName创建RoleRunAsToken,对Servlet中的runAsToken,我看的Jetty版本也还没有实现完成。
public UserIdentity newUserIdentity(final Subject subject, final Principal userPrincipal, final String[] roles) {
return new DefaultUserIdentity(subject,userPrincipal,roles);
}
public RunAsToken newRunAsToken(String runAsName) {
return new RoleRunAsToken(runAsName);
}
LoginService实现
在Jetty中,LoginService用来验证给定的用户名和证书信息(如密码),即对应的login方法;以及验证给定的UserIdentity,即对应的validate方法;其Name属性用于标识实例本身(即作为当前使用的realm name);另外IdentityService用于根据加载的用户名和证书信息创建UserIdentity实例。
public interface LoginService {
String getName();
UserIdentity login(String username,Object credentials);
boolean validate(UserIdentity user);
IdentityService getIdentityService();
void setIdentityService(IdentityService service);
void logout(UserIdentity user);
}
为了验证用户提供的用户名和证书的正确性和合法性,需要有一个地方用来存储定义好的正确的用户名以及对应的证书信息(如密码等),Jetty提供了DB、Properties文件、JAAS、SPNEGO作为用户信息源的比较。对于DB或Properties文件方式存储用户信息,如果每次的验证都去查询数据库或读取文件内容,效率会很低,因而还有一种实现方式是将数据库或文件中定义的用户信息预先的加载到内存中,这样每次验证只需要读取内存即可,这种方式的实现性能会提高很多,但是这样就无法动态的修改用户信息,并且如果用户信息很多,会占用很多的内存,目前Jetty采用后者实现,其中数据库存储用户信息有两个:JDBCLoginService以及DataSourceLoginService,Properties文件存储对应的实现是HashLoginService,它们都继承自MappedLoginService。在MappedLoginService中保存了一个ConcurrentMap<String, UserIdentity>实例,它是一个UserName到UserIdentity的映射,在该实例start时,它会从底层的数据源中加载用户信息,对HashLoginService,它会从config指定的Properties文件中加载用户信息,并填充ConcurrentMap<String, UserIdentity>,其中Properties文件的格式为:<username>=credential, role1, role2, ....如果credential以"MD5:"开头,表示它是MD5数据,如果以"CRYPT:"开头,表示它是crypt数据,否则表示它是密码字符;如果以存在的用户不在新读取的用户列表中,则将其移除,因为HashLoginService还可以启动一个线程以隔一定的时间重新加载文件中的内容,以处理文件更新的问题。在MappedLoginService中还定义了几个Principal的实现类:KnownUser、RolePrincipal、Anonymous等,在添加加载的用户时,使用KnownUser保存username和credential信息,并将该Principal添加到Subject的Principals集合中,同时对每个role创建RolePrincipal,并添加到Subject的Principals集合中,而将credential添加到Subject的PrivateCredentials集合中,使用IdentityService创建UserIdentity,并添加到ConcurrentMap<String, UserIdentity>中。在login验证中,首先使用传入的username查找存在的UserIdentity,并使用找到的UserIdentity中的Principal的check方法验证传入的credential,如果验证失败,返回null(即调用Credential的check方法:Password/MD5/Crypt)。对DataSourceLoginService和JDBCLoginService只是从数据库中加载用户信息,不详述。而JAASLoginService和SpnegoLoginService也只是使用各自的协议进行验证,不细述。
Authenticator实现
Authenticator用于验证传入的ServletRequest、ServletResponse是否包含正确的认证信息。其接口定义如下:
public interface Authenticator {
// Jetty支持BASIC、FORM、DIGEST、CLIENT_CERT、SPNEGO的认证,该方法返回其中的一种,或用于自定义的方法。
String getAuthMethod();
// 设置配置信息(SecurityHandler继承自AuthConfiguration接口):AuthMethod、RealmName、InitParameters、LoginService、IdentityService、IsSessionRenewedOnAuthentication
void setConfiguration(AuthConfiguration configuration);
// 验证逻辑的实现方法,其中mandatory若为false表示当前资源有没有配置role信息,或者@ServletSecurity中的@HttpConstraint的EmptyRoleSemantic被配置为PERMIT,此时返回Deferred类型的Authentication,如果不手动的调用其authenticate或login方法,就不会对该请求进行验证。
// 对BasicAuthenticator的实现,它从Authorization请求头中获取认证信息(用户名和用户密码,使用":"分割,并且使用Base64编码),调用LoginService进行认证,当认证通过时,如果配置了renewSession为true,则将HttpSession中的所有属性更新一遍,并且添加(org.eclipse.jetty.security.secured, True) entry,并使用UserIdentity以及AuthMethod创建UserAuthentication返回。如果认证失败,则返回401 Unauthorized错误,并且在相应消息中包含头:WWW-Authenticate: basic realm=<LoginService.name>
// 对FormAuthenticator的实现,它首先要配置formLoginPage、formLoginPath(默认j_security_check)、formErrorPage、formErrorPath;只有当前请求URL是formLoginPath时,从j_username和j_password请求参数中获取username和password信息,使用LoginService验证,如果验证通过且这个请求是因为之前请求其他资源重定向过来的,这重定向到之前的URL,创建一个SessionAuthentication放入HttpSession中,并返回一个新创建的FormAuthentication;如果验证失败,如果没定义formErrorPage,返回403 Forbidden相应,否则重定向或forward到formErrorPage;对于其他URL请求,查看在当前Session中是否存在已认证的Authentication,如果有,但是重新验证缓存的Authentication失败,则将这个Authentication从HttpSession中移除;否则返回这个Session中的Authentication;对于其他情况,表示当前请求需要认证后才能访问,此时保存当前请求URI以及POST数据到Session中,以在认证之后可以直接跳转,然后重定向或forward到formLoginPage中。
// 对DigestAuthenticator的实现类似BasicAuthenticator,只是它使用Digest的方式对认证数据进行加密和解密。
// 对ClientCertAuthenticator则采用客户端证书的方式认证,SpnegoAuthenticator使用SPNEGO方式认证,JaspiAuthenticator使用JASPI方式认证。
Authentication validateRequest(ServletRequest request, ServletResponse response, boolean mandatory) throws ServerAuthException;
// 只用于JaspiAuthenticator,用于所有后继handler处理完成后对ServletRequest、ServletResponse、User的进一步处理,目前不了解JASPI的协议逻辑,因而不了解具体的用途。
boolean secureResponse(ServletRequest request, ServletResponse response, boolean mandatory, User validatedUser) throws ServerAuthException;
}
SecurityHandler与ConstraintSecurityHandler实现
SecurityHandler继承自HandlerWrapper,并实现了Authenticator.AuthConfiguration接口,因而它包含了realm、authMethod、initParameters、loginService、identityService、renewSession等字段,在其start时,它会首先从ServletContext的InitParameters中导入org.eclipse.jetty.security.*属性的值到其InitParameters中,如果LoginService为null,则从Server中查找一个已经注册的LoginService,使用Authenticator.Factory根据AuthMethod创建对应的Authenticator实例。
ConstraintSecurityHandler继承自SecurityHandler类,它定义了ConstraintMapping列表、所有定义的role、以及pathSpec到Map<String, RoleInfo>(key为httpMethod,RoleInfo包含UserDataConstraint枚举类型和roles集合)的映射,其中ConstraintMapping中保存了method、methodOmissions、pathSpec、Constraint(Constraint中包含了name、roles、dataConstraint等信息),ConstraintMapping在解析web.xml文件时添加,它对应<security-constraint>下的配置,如auth-constraint下的role-name配置对应roles数组,user-data-contraint对应dataConstraint,web-resource-name对应name,http-method对应method,url-pattern对应pathSpec;在每次添加ConstraintMapping时都会更新roles列表以及pathSpec到Map<String, RoleInfo>的映射。
在SecurityHandler的handle方法中,它只需要对REQUEST、ASYNC类型的DispatcherType需要验证:它首先根据pathInContext和Request实例查找RoleInfo信息;如果RoleInfo处于forbidden状态,发送403 Forbidden相应,如果DataConstraint配置了Intergal、Confidential,但是Connector中没有配置相应的port,则发送403 Forbidden相应,否则重定向请求到Integral、Confidential对应的URL;对没有验证过的请求调用Authenticator.validateRequest()对请求进行验证;如果验证的结果是Authentication.ResponseSent,设置Request的handled为true,如果为Authentication.User,表示认证成功,设置该Authentication到Request中,并检查role,即检查当前User是否处于RoleInfo中的role集合中,如果不是,发送403 Forbidden响应,否则调用下一个handler的handle方法,之后调用Authenticator.secureResponse()方法;如果验证结果是Authentication.Deferred,在调用下一个handler的handle方法后调用Authenticator.secureResponse()方法;否则直接调用Authenticator.secureResponse()方法。