概述
Jetty的強大之處在于可以自由的配置某些組建的存在與否,以提升性能,減少復(fù)雜度,而其本身也因為這種特性而具有很強的可擴展性。SecurityHandler就是Jetty對Servlet中Security框架部分的實現(xiàn),并可以根據(jù)實際需要裝卸和替換。Servlet的安全框架主要有兩個部分:數(shù)據(jù)傳輸?shù)陌踩约皵?shù)據(jù)授權(quán),對數(shù)據(jù)傳輸?shù)陌踩梢允褂肧SL對應(yīng)的Connector實現(xiàn),而對于數(shù)據(jù)授權(quán)安全,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的映射關(guān)系,從文檔上,這里的role-name是Servlet中使用的名字,而role-link是Container中使用的名字,感覺很模糊,從Jetty的角度,role-name是web.xml中在<security-constraint>/<auth-constraint>/<role-name>中對一個URL Pattern的role定義,而role-link則是UserIdentity中roles數(shù)組的值,而UserIdentity是LoginService中創(chuàng)建的,它從文件、數(shù)據(jù)庫等加載已定義的user的信息:用戶名、密碼、它隸屬的role等,如果Servlet中沒有定義role-name到role-link的映射,則直接使用role-name去UserIdentity中比較role信息。
關(guān)于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的定義如下:<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>
<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>
<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實現(xiàn)概述和類圖
在Jetty中,使用Authenticator接口抽象不同用戶登陸驗證的邏輯;使用LoginService接口抽象對用戶名、密碼的驗證;使用UserIdentity保存內(nèi)部定義的一個用戶的用戶名、密碼、role集合;使用ConstraintMapping保存URL Pattern到role集合的映射;使用UserIdentity.Scope保存一個Servlet中role-name到role-link的映射。他們的類圖如下:
UserIdentity實現(xiàn)
UserIdentity表示一個用戶的認證信息,它包含Subject和UserPrincipal,其中Subject是Java Security框架定義的類型,而UserPrincipal則用于存儲用戶名以及認證信息,在Jetty中一般使用KnownUser來存儲,它包含了UserName以及Credential實例,其中Credential可以是Crypt、MD5、Password等。在Credential中定義了check方法用于驗證傳入的credential是否是正確的。IdentityService實現(xiàn)
IdentityService我猜原本用于將UserIdentity、RunAsToken和當前Thread關(guān)聯(lián)在一起,以及創(chuàng)建UserIdentity、RunAsToken,然而我看的版本中,DefaultIdentityService貌似還沒有實現(xiàn)完成,目前只是根據(jù)提供的Subject、Principal、roles創(chuàng)建DefaultUserIdentity實例,以及使用runAsName創(chuàng)建RoleRunAsToken,對Servlet中的runAsToken,我看的Jetty版本也還沒有實現(xiàn)完成。 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);
}
return new DefaultUserIdentity(subject,userPrincipal,roles);
}
public RunAsToken newRunAsToken(String runAsName) {
return new RoleRunAsToken(runAsName);
}
LoginService實現(xiàn)
在Jetty中,LoginService用來驗證給定的用戶名和證書信息(如密碼),即對應(yīng)的login方法;以及驗證給定的UserIdentity,即對應(yīng)的validate方法;其Name屬性用于標識實例本身(即作為當前使用的realm name);另外IdentityService用于根據(jù)加載的用戶名和證書信息創(chuàng)建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);
}
String getName();
UserIdentity login(String username,Object credentials);
boolean validate(UserIdentity user);
IdentityService getIdentityService();
void setIdentityService(IdentityService service);
void logout(UserIdentity user);
}
為了驗證用戶提供的用戶名和證書的正確性和合法性,需要有一個地方用來存儲定義好的正確的用戶名以及對應(yīng)的證書信息(如密碼等),Jetty提供了DB、Properties文件、JAAS、SPNEGO作為用戶信息源的比較。對于DB或Properties文件方式存儲用戶信息,如果每次的驗證都去查詢數(shù)據(jù)庫或讀取文件內(nèi)容,效率會很低,因而還有一種實現(xiàn)方式是將數(shù)據(jù)庫或文件中定義的用戶信息預(yù)先的加載到內(nèi)存中,這樣每次驗證只需要讀取內(nèi)存即可,這種方式的實現(xiàn)性能會提高很多,但是這樣就無法動態(tài)的修改用戶信息,并且如果用戶信息很多,會占用很多的內(nèi)存,目前Jetty采用后者實現(xiàn),其中數(shù)據(jù)庫存儲用戶信息有兩個:JDBCLoginService以及DataSourceLoginService,Properties文件存儲對應(yīng)的實現(xiàn)是HashLoginService,它們都繼承自MappedLoginService。在MappedLoginService中保存了一個ConcurrentMap<String, UserIdentity>實例,它是一個UserName到UserIdentity的映射,在該實例start時,它會從底層的數(shù)據(jù)源中加載用戶信息,對HashLoginService,它會從config指定的Properties文件中加載用戶信息,并填充ConcurrentMap<String, UserIdentity>,其中Properties文件的格式為:<username>=credential, role1, role2, ....如果credential以"MD5:"開頭,表示它是MD5數(shù)據(jù),如果以"CRYPT:"開頭,表示它是crypt數(shù)據(jù),否則表示它是密碼字符;如果以存在的用戶不在新讀取的用戶列表中,則將其移除,因為HashLoginService還可以啟動一個線程以隔一定的時間重新加載文件中的內(nèi)容,以處理文件更新的問題。在MappedLoginService中還定義了幾個Principal的實現(xiàn)類:KnownUser、RolePrincipal、Anonymous等,在添加加載的用戶時,使用KnownUser保存username和credential信息,并將該Principal添加到Subject的Principals集合中,同時對每個role創(chuàng)建RolePrincipal,并添加到Subject的Principals集合中,而將credential添加到Subject的PrivateCredentials集合中,使用IdentityService創(chuàng)建UserIdentity,并添加到ConcurrentMap<String, UserIdentity>中。在login驗證中,首先使用傳入的username查找存在的UserIdentity,并使用找到的UserIdentity中的Principal的check方法驗證傳入的credential,如果驗證失敗,返回null(即調(diào)用Credential的check方法:Password/MD5/Crypt)。對DataSourceLoginService和JDBCLoginService只是從數(shù)據(jù)庫中加載用戶信息,不詳述。而JAASLoginService和SpnegoLoginService也只是使用各自的協(xié)議進行驗證,不細述。
Authenticator實現(xiàn)
Authenticator用于驗證傳入的ServletRequest、ServletResponse是否包含正確的認證信息。其接口定義如下:public interface Authenticator {
// Jetty支持BASIC、FORM、DIGEST、CLIENT_CERT、SPNEGO的認證,該方法返回其中的一種,或用于自定義的方法。
String getAuthMethod();
// 設(shè)置配置信息(SecurityHandler繼承自AuthConfiguration接口):AuthMethod、RealmName、InitParameters、LoginService、IdentityService、IsSessionRenewedOnAuthentication
void setConfiguration(AuthConfiguration configuration);
// 驗證邏輯的實現(xiàn)方法,其中mandatory若為false表示當前資源有沒有配置role信息,或者@ServletSecurity中的@HttpConstraint的EmptyRoleSemantic被配置為PERMIT,此時返回Deferred類型的Authentication,如果不手動的調(diào)用其authenticate或login方法,就不會對該請求進行驗證。
// 對BasicAuthenticator的實現(xiàn),它從Authorization請求頭中獲取認證信息(用戶名和用戶密碼,使用":"分割,并且使用Base64編碼),調(diào)用LoginService進行認證,當認證通過時,如果配置了renewSession為true,則將HttpSession中的所有屬性更新一遍,并且添加(org.eclipse.jetty.security.secured, True) entry,并使用UserIdentity以及AuthMethod創(chuàng)建UserAuthentication返回。如果認證失敗,則返回401 Unauthorized錯誤,并且在相應(yīng)消息中包含頭:WWW-Authenticate: basic realm=<LoginService.name>
// 對FormAuthenticator的實現(xiàn),它首先要配置formLoginPage、formLoginPath(默認j_security_check)、formErrorPage、formErrorPath;只有當前請求URL是formLoginPath時,從j_username和j_password請求參數(shù)中獲取username和password信息,使用LoginService驗證,如果驗證通過且這個請求是因為之前請求其他資源重定向過來的,這重定向到之前的URL,創(chuàng)建一個SessionAuthentication放入HttpSession中,并返回一個新創(chuàng)建的FormAuthentication;如果驗證失敗,如果沒定義formErrorPage,返回403 Forbidden相應(yīng),否則重定向或forward到formErrorPage;對于其他URL請求,查看在當前Session中是否存在已認證的Authentication,如果有,但是重新驗證緩存的Authentication失敗,則將這個Authentication從HttpSession中移除;否則返回這個Session中的Authentication;對于其他情況,表示當前請求需要認證后才能訪問,此時保存當前請求URI以及POST數(shù)據(jù)到Session中,以在認證之后可以直接跳轉(zhuǎn),然后重定向或forward到formLoginPage中。
// 對DigestAuthenticator的實現(xiàn)類似BasicAuthenticator,只是它使用Digest的方式對認證數(shù)據(jù)進行加密和解密。
// 對ClientCertAuthenticator則采用客戶端證書的方式認證,SpnegoAuthenticator使用SPNEGO方式認證,JaspiAuthenticator使用JASPI方式認證。
Authentication validateRequest(ServletRequest request, ServletResponse response, boolean mandatory) throws ServerAuthException;
// 只用于JaspiAuthenticator,用于所有后繼handler處理完成后對ServletRequest、ServletResponse、User的進一步處理,目前不了解JASPI的協(xié)議邏輯,因而不了解具體的用途。
boolean secureResponse(ServletRequest request, ServletResponse response, boolean mandatory, User validatedUser) throws ServerAuthException;
}
// Jetty支持BASIC、FORM、DIGEST、CLIENT_CERT、SPNEGO的認證,該方法返回其中的一種,或用于自定義的方法。
String getAuthMethod();
// 設(shè)置配置信息(SecurityHandler繼承自AuthConfiguration接口):AuthMethod、RealmName、InitParameters、LoginService、IdentityService、IsSessionRenewedOnAuthentication
void setConfiguration(AuthConfiguration configuration);
// 驗證邏輯的實現(xiàn)方法,其中mandatory若為false表示當前資源有沒有配置role信息,或者@ServletSecurity中的@HttpConstraint的EmptyRoleSemantic被配置為PERMIT,此時返回Deferred類型的Authentication,如果不手動的調(diào)用其authenticate或login方法,就不會對該請求進行驗證。
// 對BasicAuthenticator的實現(xiàn),它從Authorization請求頭中獲取認證信息(用戶名和用戶密碼,使用":"分割,并且使用Base64編碼),調(diào)用LoginService進行認證,當認證通過時,如果配置了renewSession為true,則將HttpSession中的所有屬性更新一遍,并且添加(org.eclipse.jetty.security.secured, True) entry,并使用UserIdentity以及AuthMethod創(chuàng)建UserAuthentication返回。如果認證失敗,則返回401 Unauthorized錯誤,并且在相應(yīng)消息中包含頭:WWW-Authenticate: basic realm=<LoginService.name>
// 對FormAuthenticator的實現(xiàn),它首先要配置formLoginPage、formLoginPath(默認j_security_check)、formErrorPage、formErrorPath;只有當前請求URL是formLoginPath時,從j_username和j_password請求參數(shù)中獲取username和password信息,使用LoginService驗證,如果驗證通過且這個請求是因為之前請求其他資源重定向過來的,這重定向到之前的URL,創(chuàng)建一個SessionAuthentication放入HttpSession中,并返回一個新創(chuàng)建的FormAuthentication;如果驗證失敗,如果沒定義formErrorPage,返回403 Forbidden相應(yīng),否則重定向或forward到formErrorPage;對于其他URL請求,查看在當前Session中是否存在已認證的Authentication,如果有,但是重新驗證緩存的Authentication失敗,則將這個Authentication從HttpSession中移除;否則返回這個Session中的Authentication;對于其他情況,表示當前請求需要認證后才能訪問,此時保存當前請求URI以及POST數(shù)據(jù)到Session中,以在認證之后可以直接跳轉(zhuǎn),然后重定向或forward到formLoginPage中。
// 對DigestAuthenticator的實現(xiàn)類似BasicAuthenticator,只是它使用Digest的方式對認證數(shù)據(jù)進行加密和解密。
// 對ClientCertAuthenticator則采用客戶端證書的方式認證,SpnegoAuthenticator使用SPNEGO方式認證,JaspiAuthenticator使用JASPI方式認證。
Authentication validateRequest(ServletRequest request, ServletResponse response, boolean mandatory) throws ServerAuthException;
// 只用于JaspiAuthenticator,用于所有后繼handler處理完成后對ServletRequest、ServletResponse、User的進一步處理,目前不了解JASPI的協(xié)議邏輯,因而不了解具體的用途。
boolean secureResponse(ServletRequest request, ServletResponse response, boolean mandatory, User validatedUser) throws ServerAuthException;
}
SecurityHandler與ConstraintSecurityHandler實現(xiàn)
SecurityHandler繼承自HandlerWrapper,并實現(xiàn)了Authenticator.AuthConfiguration接口,因而它包含了realm、authMethod、initParameters、loginService、identityService、renewSession等字段,在其start時,它會首先從ServletContext的InitParameters中導(dǎo)入org.eclipse.jetty.security.*屬性的值到其InitParameters中,如果LoginService為null,則從Server中查找一個已經(jīng)注冊的LoginService,使用Authenticator.Factory根據(jù)AuthMethod創(chuàng)建對應(yīng)的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文件時添加,它對應(yīng)<security-constraint>下的配置,如auth-constraint下的role-name配置對應(yīng)roles數(shù)組,user-data-contraint對應(yīng)dataConstraint,web-resource-name對應(yīng)name,http-method對應(yīng)method,url-pattern對應(yīng)pathSpec;在每次添加ConstraintMapping時都會更新roles列表以及pathSpec到Map<String, RoleInfo>的映射。
在SecurityHandler的handle方法中,它只需要對REQUEST、ASYNC類型的DispatcherType需要驗證:它首先根據(jù)pathInContext和Request實例查找RoleInfo信息;如果RoleInfo處于forbidden狀態(tài),發(fā)送403 Forbidden相應(yīng),如果DataConstraint配置了Intergal、Confidential,但是Connector中沒有配置相應(yīng)的port,則發(fā)送403 Forbidden相應(yīng),否則重定向請求到Integral、Confidential對應(yīng)的URL;對沒有驗證過的請求調(diào)用Authenticator.validateRequest()對請求進行驗證;如果驗證的結(jié)果是Authentication.ResponseSent,設(shè)置Request的handled為true,如果為Authentication.User,表示認證成功,設(shè)置該Authentication到Request中,并檢查role,即檢查當前User是否處于RoleInfo中的role集合中,如果不是,發(fā)送403 Forbidden響應(yīng),否則調(diào)用下一個handler的handle方法,之后調(diào)用Authenticator.secureResponse()方法;如果驗證結(jié)果是Authentication.Deferred,在調(diào)用下一個handler的handle方法后調(diào)用Authenticator.secureResponse()方法;否則直接調(diào)用Authenticator.secureResponse()方法。