概述
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()方法。
posted on 2014-05-18 22:02
DLevin 閱讀(3344)
評論(2) 編輯 收藏 所屬分類:
Jetty