Tomcat安全域?qū)崿F(xiàn)細節(jié)分析
一、簡介
為了實現(xiàn) Servlet 規(guī)范中規(guī)定的對于特定資源的保護,Tomcat 提供了安全域的功能實現(xiàn)。如果應用使用了安全域保護系統(tǒng)資源,安全域就需要對每一次的訪問負責,結合 Tomcat的訪問流程,可以想到安全域的認證器是作為一個閥門(Valve)來實現(xiàn)的。
Tomcat實現(xiàn)了多種多樣的安全域滿足不同的用戶需求:
- 配置快捷、利用數(shù)據(jù)源進行認證的DataSourceRealm,
- 更為簡易的JDBCRealm,
- 通過第三方Ldap服務器認證的JNDIRealm,
- 限制失敗次數(shù)防止暴力破解的LockOutRealm,
- 通過文本文件配置用戶信息、一般用于開發(fā)、測試的MemoryRealm,
Tomcat安全域的默認實現(xiàn)UserDatabaseRealm、靈活的用戶自定義的JaasRealm,他們都實現(xiàn)了Realm接口,并擁有共同的父類RealmBase。
二、Realm接口
Realm接口是安全域模塊的核心接口,其提供了幾個重要的方法:authenticate()方法以及多個重載用于提供用戶名、密碼等方式的認證功能;hasResourcePermission()方法用于認證器(Authenticator)判斷當前角色是否有權限訪問資源,該方法通過調(diào)用hasRole()等方法進行判斷;而hasUserDataPermission()方法則對數(shù)據(jù)傳輸層的傳輸要求進行判斷。
通過上述幾個方法,就能夠大致勾勒出一次通過安全域的請求訪問的流程:用戶請求資源,經(jīng)過各層閥門后走到安全域(認證器),安全域首先要判斷對目標資源的請求是否符合數(shù)據(jù)傳輸層的要求;然后通過authenticate()方法判斷當前用戶是否經(jīng)過認證,如果沒有認證則向用戶請求認證信息;通過認證后,則進行角色和權限的判斷;最后,根據(jù)認證結果繼續(xù)請求流程或者直接返回請求拒絕信息。
Realm接口使用了Principal、SecurityConstraint、X509Certificate等接口或類,Principal是jdk api定義的表示主體的抽象概念,X509Certificate是jdk api定義的X.509 證書的抽象類,該類提供了一種訪問 X.509 證書所有屬性的標準方式,SecurityConstraint則是tomcat定義的對web.xml中相應
三、RealmBase抽象類
RealmBase類對Realm接口中的大部分方法進行了實現(xiàn),前面說到安全域的authenticate()認證方法提供了多種重載,其目的就是為了適用各種環(huán)境下的認證方式,畢竟并不是所有的認證信息都可以用用戶名和密碼的方式傳遞的。
例如,冗長的認證方法authenticate(String username, String clientDigest,String nonce, String nc, String cnonce,String qop, String realm,String md5a2)是為了Digest認證而設計的,而簡約的authenticate(X509Certificate certs[])方法則對應https協(xié)議下的證書認證。由于特殊的需求,RealmBase的部分子類仍會重寫authenticate()方法。相關的認證方法及實現(xiàn),在后續(xù)介紹HTTP認證方式時再詳細介紹。
在進行真正的認證工作前,有一步非常重要的校驗工作,即當前請求是否滿足定義的支持的連接類型,該處的邏輯處理由hasUserDataPermission()方法完成。
如果在web.xml的user-data-constraint節(jié)點定義了連接類型,而且連接類型不為NONE的話,Tomcat則會認為該請求需要建立在安全的連接之上,按照servlet規(guī)范定義,通過查找當前請求連接器的HTTPS重定向端口,該請求通過response.sendRedirect()的方式跳轉(zhuǎn)到https請求。如果當前的請求連接器并沒有配置有效的HTTPS重定向端口,則返回403 (SC_FORBIDDEN)狀態(tài)碼。
如果通過了前面提到的安全域認證,這說明了用戶提供的用戶名、密碼等憑證是有效的,但這還不能夠說明當前用戶對目標資源具有訪問的權限,所以要經(jīng)過
hasResourcePermission()方法,進行用戶“授權”的工作。前面提到,類SecurityConstraint是對web.xml中相應
- <security-constraint>
- <web-resource-collection>
- <web-resource-name>Protected Area</web-resource-name>
- <url-pattern>/adminInfo/*</url-pattern>
- <http-method>GET</http-method>
- <http-method>POST</http-method>
- </web-resource-collection>
- <auth-constraint>
- <role-name>admin</role-name>
- </auth-constraint>
- <user-data-constraint>
- <transport-guarantee>CONFIDENTIAL</transport-guarantee>
- </user-data-constraint>
- </security-constraint>
為了進行最后的授權工作,需要將用戶的當前角色與web.xml定義的角色進行比對。不要小看短短的幾行配置文件,Servlet規(guī)范進行了詳盡的描述,Tomcat也遵循規(guī)范進行了細致的實現(xiàn)。例如:role-name的配置,通常來說,會按照業(yè)務需求將其配置為具有相應權限的用戶名稱,但對于通配符“*”賦予了特殊的含義。
“*”表示web.xml中定義的所有用戶,“**”則表示所有通過了認證的用戶。同時對于role-name為空的情況,則任何用戶都不能訪問相應的資源。
在認證階段,如果用戶沒有通過認證,或者是第一次訪問,則會拒絕該請求并返回401(SC_UNAUTHORIZED)狀態(tài)碼(FORM類型的認證除外,因為要跳轉(zhuǎn)至登錄頁面);如果用戶角色沒有滿足預先定義的權限,則會拒絕該請求并返回403 (SC_FORBIDDEN)狀態(tài)碼。
四、HTTP認證方法與實現(xiàn)
下面簡單介紹JavaEE平臺支持的四種認證機制:
- Basic authentication
- Form-based authentication
- Digest authentication
- Client authentication
Tomcat為實現(xiàn)上述認證機制,提供了多種認證器,如下圖所示。認證器位處于安全域前端,對于不同類型的HTTP認證方式,先由各認證器根據(jù)相應的規(guī)范對客戶端發(fā)送至服務端的信息進行解析,然后再交由安全域進行處理,例如前面提到的對當前請求是否滿足定義的支持連接類型的判斷就是由認證器發(fā)起的。各認證器都繼承了AuthenticatorBase抽象類,其中重要的authenticate()方法由各實現(xiàn)類進行具體的實現(xiàn)。
BASIC認證
BASIC基本認證是HTTP1.0標準提出的認證方式,規(guī)范中即提出BASIC認證是不安全的用戶認證方案,并支持在目前日益嚴重的網(wǎng)絡安全問題面前采用更加復雜的其他認證方式及加密機制。因此,對于非SSL層請求的認證,不建議使用BASIC認證;但如果請求是在安全的傳輸層上,傳輸層提供了安全保障,即使是簡單加密的BASIC認證也可以認為是安全的。BASIC認證的規(guī)則如下:
- 客戶端訪問受保護的資源。
- 服務器返回401 Unauthorized狀態(tài),響應頭信息如下圖所示,其中WWW-Authenticate:Basic realm="MyRealm"表示該資源的受保護信息。
- 瀏覽器根據(jù)響應彈出窗口,提示用戶輸入用戶名和密碼。
- 瀏覽器將客戶端將輸入的用戶名、密碼用Base64算法進行加密后發(fā)送給服務器。例如,使用用戶名、密碼都是“java”進行登錄,瀏覽器則發(fā)送的請求頭中包含“Authorization: Basic amF2YTpqYXZh”,其中“amF2YTpqYXZh”是用戶名、密碼組成的字符串“java:java”進行Base64加密得到的結果。
- 如果認證成功,則返回相應的受保護資源。如果認證失敗,則仍返回401 Unauthorized狀態(tài),要求重新進行認證。
可以簡單了解一下Tomcat的BASIC認證器類BasicAuthenticator的關鍵代碼:
- public boolean authenticate(Request request, HttpServletResponse response)
- throws IOException {
- if (checkForCachedAuthentication(request, response, true)) {
- return true;
- }
- // Validate any credentials already included with this request
- MessageBytes authorization =
- request.getCoyoteRequest().getMimeHeaders()
- .getValue("authorization");//獲取authorization請求頭
- if (authorization != null) {
- authorization.toBytes();
- ByteChunk authorizationauthorizationBC = authorization.getByteChunk();
- BasicCredentials credentials = null;
- try {
- credentials = new BasicCredentials(authorizationBC);//Base64解密用戶名密碼
- String username = credentials.getUsername();
- String password = credentials.getPassword();
- Principal principal = context.getRealm().authenticate(username, password);//安全域認證
- if (principal != null) {//認證成功
- register(request, response, principal,
- HttpServletRequest.BASIC_AUTH, username, password);
- return (true);
- }
- }
- catch (IllegalArgumentException iae) {
- if (log.isDebugEnabled()) {
- log.debug("Invalid Authorization" + iae.getMessage());
- }
- }
- }
- // the request could not be authenticated, so reissue the challenge
- StringBuilder value = new StringBuilder(16);//認證失敗返回重新認證
- value.append("Basic realm=\"");
- value.append(getRealmName(context));
- value.append('\"');
- response.setHeader(AUTH_HEADER_NAME, value.toString());
- response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
- return (false);
- }
該認證方法的基本邏輯還是比較清晰的:
- 首先判斷是否已經(jīng)進行了認證,如果已經(jīng)認證則沒有必要重復認證,返回即可。
- 嘗試取出“authorization”請求頭,如果沒有該請求頭,則返回401Unauthorized狀態(tài)碼以及受保護資源的安全域信息。
- 對“authorization”請求頭進行BASIC64解密,然后使用“:”切分為用戶名和密碼。
- 使用安全域?qū)τ脩裘⒚艽a進行真正的認證工作,如果認證成功,將當前用戶信息進行緩存。
Form認證
Basic認證和后面介紹的Digest認證都是rfc2616中明確定義的認證方式,拋開安全性,兩者在實際使用中均有一個嚴重的缺點,即用戶UI幾乎無設計的問題。在用戶體驗至高無上的互聯(lián)網(wǎng)時代,UI界面占據(jù)著很大的比重,而Basic和Digest認證由于其自身的設計,各瀏覽器的實現(xiàn)都是彈出一個無所謂美觀的對話框,對用戶體驗有很大的影響。Form認證中定義了采集用戶信息的登錄頁面、登錄失敗頁面,通過用戶自定義實現(xiàn)這兩個頁面,能夠完成美觀的登錄操作。在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規(guī)范中規(guī)定,使用Form認證時,表單提交的action必須為j_security_check,而獲取登錄信息的字段必須為j_username和j_password,這樣的約定省去了相關字段的配置工作。From認證的邏輯也很清晰,下面抽取tomcat的關鍵代碼進行解釋:
- 查看是否已經(jīng)對當前用戶進行了認證,避免重復認證造成資源浪費:checkForCachedAuthentication(request, response, true)。
- 如果沒有認證,則需要保存當前用戶需要保存的頁面:saveRequest(request, session),然后跳轉(zhuǎn)到登錄頁面:forwardToLoginPage(request, response, config)。
- 用戶提交了用戶名和密碼,進行認證工作:principal = realm.authenticate(username, password);如果認證失敗,則跳轉(zhuǎn)至失敗頁面:forwardToErrorPage(request, response, config);如果認證成功,則跳轉(zhuǎn)至第二步保存的頁面:response.sendRedirect(response.encodeRedirectURL(uri))。
- 瀏覽器接收到302重定向狀態(tài)碼后,將頁面跳轉(zhuǎn)至最初訪問的頁面。
- 再次走進Form認證器的認證流程,通過判斷條件matchRequest(request)將認證主體(Principal)保存在Request和Session中,判斷條件為:已經(jīng)通過了認證;存在一個已保存的頁面且與當前請求頁面路徑相同。然后將本次請求的所有信息都重置為最初的請求信息:restoreRequest(request, session)。此后的訪問在第一步即直接返回了。
有興趣的讀者可以深入的了解一下上述的關鍵代碼的實現(xiàn)。
Digest認證
Digest摘要認證是在HTTP1.1中提出的替代Basic認證的方法。由于Basic認證使用的的Base64加密幾乎等于明文傳輸,安全性低,Digest認證提供了一種不使用明文發(fā)送用戶名密碼的方式。當然,HTTP1.1標準也提出,摘要訪問認證語法("Digest Access Authentication scheme")并非要提供一個網(wǎng)絡安全的完美解決方案,其目的僅僅是為了避免深受詬病的Basic認證的諸多缺點。因此,不管怎樣,相比較Basic認證,Digest認證的安全性還是有所提高的。Digest認證的規(guī)則如下:
1.客戶端訪問受保護的資源。
2.服務器返回401 Unauthorized狀態(tài),響應頭信息如下圖所示,其中
- WWW-Authenticate:Digest realm="MyRealm", qop="auth", nonce="1454307975468:a0aefce3e84d69723e6f04fda5674ad0", opaque="23BB4CB60BFE2CD08B490A16B86C9661"
表示相關的安全域信息、隨機數(shù)信息(nonce)等。
3.瀏覽器根據(jù)響應彈出窗口,提示用戶輸入用戶名和密碼。
4.瀏覽器將客戶端將輸入的用戶名以明文的方式、密碼等其他信息以摘要的方式返回給服務端。
5.服務端將用戶名、正確的密碼等信息按規(guī)則進行摘要加密,與客戶端提供的信息進行比對。如果認證成功,則返回相應的受保護資源。如果認證失敗,則仍返回401 Unauthorized狀態(tài),要求重新進行認證。
其中的隨機數(shù)nonce的值應當是永不重復的數(shù)值,下面看一下tomcat是怎樣簡單的實現(xiàn)并保證唯一性的:
- protected String generateNonce(Request request) {
- long currentTime = System.currentTimeMillis();
- synchronized (lastTimestampLock) {//加鎖,并發(fā)下也不會取到相同的時間
- if (currentTime > lastTimestamp) {
- lastTimestamp = currentTime;
- } else {
- currentTime = ++lastTimestamp;
- }
- }
- String ipTimeKey =
- request.getRemoteAddr() + ":" + currentTime + ":" + getKey();
- byte[] buffer = ConcurrentMessageDigest.digestMD5(
- ipTimeKey.getBytes(StandardCharsets.ISO_8859_1));
- String nonce = currentTime + ":" + MD5Encoder.encode(buffer);
- NonceInfo info = new NonceInfo(currentTime, getNonceCountWindowSize());
- synchronized (nonces) {
- nonces.put(nonce, info);
- }
- return nonce;
- }
Tomcat使用了客戶端IP地址、當前時間和Digest認證器的一個固定的key進行拼接然后進行MD5加密等最終生成nonce的。其中的固定值key是tomcat在初始化該Digest認證器時,使用的是與session id相同的方法生成,其中具體使用了JDK提供的
java.security.SecureRandom隨機數(shù)等,與UUID的生成方式相似,感興趣的讀者可以分析一下JDK中UUID生成唯一值的算法。可以看到,tomcat在生成nonce隨機數(shù)時考慮了三方面的可能性,以保證隨機數(shù)nonce的唯一性:
- 生產(chǎn)環(huán)境中IP地址的唯一性;
- 進行http訪問時當前時間可能存在由于并發(fā)導致的不唯一性,此時會在同步塊中進行對比以確保唯一。
- 同一IP地址下不同的tomcat或者不同應用的電子標簽key的唯一性。
上面三個唯一性結合進行MD5加密,保證了任何可能環(huán)境下的唯一性。
用戶提交用戶名、密碼信息后,Digest認證器將獲取的相關Authorization請求頭信息,正如authenticate(String username, String clientDigest,String nonce, String nc, String cnonce,String qop, String realm,String md5a2)方法中的各項參數(shù),交由相應的安全域進行處理。其主要思想就是將用戶提供的根據(jù)規(guī)則進行摘要加密生成的字符串,與服務端使用正確的密碼、相同規(guī)則生成的字符串進行比對。如果用戶名、密碼正確,客戶端提供的字符串自然與服務端生成的字符串相同,則認證通過。在此不在進行進一步闡述。
Client認證
前面提到不管是明文傳輸?shù)腂asic認證和Form認證,還是經(jīng)過摘要加密的Digest認證,都不能很好的解決網(wǎng)絡安全問題。Client認證依賴于HTTPS,因此是Java EE安全規(guī)范中安全性最高的一種認證方式。使用Client認證需要在web.xml中配置如下:
- <login-config>
- <auth-method>CLIENT-CERT</auth-method>
- </login-config>
HTTPS通道相關知識以及如何在tomcat配置HTTPS通道證書以及信任證書讀者可自行Google,下面重點分析tomcat證書與安全域的關系。
由于Client認證依賴于HTTPS,如果對相應資源的請求不在HTTPS通道上,tomcat就無法獲取到客戶端證書,也就無法通過證書進一步對用戶身份進行認證。此時,瀏覽器獲得的響應如下圖所示:
Tomcat在請求流程處理中已經(jīng)將證書解析并保存在了Request對象的
"javax.servlet.request.X509Certificate"屬性中,證書類使用了JDK提供的抽象類
java.security.cert.X509Certificate,并使用X509Certificate.getSubjectDN().getName()方法作為安全域的默認登錄用戶名。登錄用戶名是通過
org.apache.catalina.realm.X509UsernameRetriever接口的實現(xiàn)類
org.apache.catalina.realm.X509SubjectDnRetriever獲取的,因此如果不想要在安全域的用戶名列表里添加過于復雜的形如“CN=localhost, OU=apache, O=apache, L=beijing, ST=bj, C=cn”的用戶名,可以定制自己的X509UsernameRetriever實現(xiàn)類。
因為用戶證書不會帶有密碼信息,而證書本身就已經(jīng)能夠表示用戶身份,所以在接下來的認證中,只需要判斷當前通過證書獲取的用戶名是否在安全域名單中就可以了。如果安全域名單中存在該證書用戶,則可以認為認證通過,可以繼續(xù)進行下面的授權工作。
五、授權
認證工作完成的是證明發(fā)起當前請求的用戶是其所聲稱的用戶,簡單的可以解釋為只要提供了正確的憑證(用戶名、密碼或證書),則認為是該用戶在請求資源。而接下來的授權工作則需要判斷該用戶是否有權限訪問該資源。通過在web.xml中配置如下參數(shù),決定哪些用戶可以訪問相關資源:
- <auth-constraint>
- <role-name>admin</role-name>
- <role-name>test</role-name>
- </auth-constraint>
前面說了,Servlet規(guī)范除了規(guī)定了role-name匹配外,也對通配符“*”做了定義:“*”表示web.xml中定義的所有用戶,“**”則表示所有通過了認證的用戶。同時對于role-name為空的情況,則任何用戶都不能訪問相應的資源。針對上述幾種特殊的情況,tomcat在授權時按續(xù)進行了處理:
- 判斷“constraint.getAuthenticatedUsers() && principal != null”,如果配置了“**”,且通過了認證,設標志位為true;否則進行下一步。
- 判斷“roles.length == 0 && !constraint.getAllRoles() &&!constraint.getAuthenticatedUsers()“,如果沒有配置role-name,且沒有配置“*”和“**”,設標志位為false;否則進行下一步。
- 判斷“principal == null”,如果沒有通過授權,設標志位為false,否則進行下一步。
- 比對當前用戶角色與配置文件中的角色,如果存在匹配角色,設標志位為true,進行下一步。沒有通過上述授權?沒關系,還有通配符“*”沒有充分派上用場。針對“*”通配符,Tomcat做出了比servlet規(guī)范更加貼合實際應用場景的擴展,分為三種情形:一,嚴格按照規(guī)范使用,“*”只表示web-app/security-role/role-name節(jié)點下的所有用戶;二,“*”表示任何通過了認證的用戶,該用法在實際應用中使用的可能更多一些,面對用戶量大且復雜的應用場景,將所有用戶角色添加到web.xml中缺乏可行性和易維護性,此處實現(xiàn)與規(guī)范定義的“**”功能相同,筆者認為其現(xiàn)實意義就是使通配符“*”的含義更加符合開發(fā)人員的使用習慣;三,上述兩種方法的折中,如果配置了web-app/security-roles下的角色,則按第一種方法使用,否則按照第二種方法使用。因此,授權流程繼續(xù):
- 判斷“allRolesMode == AllRolesMode.AUTH_ONLY_MODE”,只需認證即可,設標志位為“true”,否則進行下一步。
- 判斷“roles.length == 0 && allRolesMode == AllRolesMode.STRICT_AUTH_ONLY_MODE”,沒有配置web-app/security-roles節(jié)點下的角色,只需認證即可,設標志位為true。
- 根據(jù)標志位返回403 Forbidden 響應或者返回用戶請求資源。
六、安全域?qū)崿F(xiàn)
前面簡單介紹了安全域的相關接口和抽象類,在具體的安全域?qū)崿F(xiàn)時只需根據(jù)相應的邏輯獲取或者比對認證信息,讀者對此應該有了大致的了解。例如,JDBC安全域在進行認證時,通過JDBC連接查詢用戶名對應的密碼,然后與客戶端密碼進行比對,返回認證結果。下面介紹一下Tomcat中很有實用價值的用于防止暴力破解用戶信息的
org.apache.catalina.realm.LockOutRealm以及跟另一規(guī)范相關的
org.apache.catalina.realm.JAASRealm。
LockOutRealm的主要工作是對先于認證工作對用戶名進行校驗,而真正的認證工作還依賴于其他的安全域?qū)崿F(xiàn),所以LockOutRealm繼承了父類
org.apache.catalina.realm.CombinedRealm,LockOutRealm后續(xù)的認證工作就交由CombinedRealm中的其他安全域進行了。
在了解LockOutRealm的實現(xiàn)之前,可以構思一下要實現(xiàn)防止暴力破解需要哪些功能:
- 首先需要一個List或者Map,用于存儲登錄失敗的用戶名稱和相關信息,而這個List或者Map又不能無限大,必須是有界的,否則會導致嚴重的內(nèi)存泄露,當然如果不受在tomcat的jvm實現(xiàn)的限制話,生產(chǎn)條件下我們可能會使用Redis。
- 需要定義用戶鎖定時的登錄失敗次數(shù)。
- 需要定義用戶解鎖時長。
- 存儲登錄失敗用戶的List或者Map由于有界,就有可能存在撐滿的情況,需定義此時的操作規(guī)則。
完成上述幾個功能點,一個比較完善的防暴力破解安全域就形成了。
下面重點看一下tomcat存儲失敗用戶的實現(xiàn):
- new LinkedHashMap<String, LockRecord>(cacheSize, 0.75f,
- true) {
- private static final long serialVersionUID = 1L;
- @Override
- protected boolean removeEldestEntry(//重寫方法,防止內(nèi)存溢出
- Map.Entry<String, LockRecord> eldest) {
- if (size() > cacheSize) {
- // Check to see if this element has been removed too quickly
- long timeInCache = (System.currentTimeMillis() -
- eldest.getValue().getLastFailureTime())/1000;
- if (timeInCache < cacheRemovalWarningTime) {//沒到時間就被移出黑名單了,要打個日志
- log.warn(sm.getString("lockOutRealm.removeWarning",
- eldest.getKey(), Long.valueOf(timeInCache)));
- }
- return true;
- }
- return false;
- }
- };
Tomcat使用了常用的LinkedHashMap存儲登錄失敗的用戶,并且重寫了removeEldestEntry方法,在JDK的實現(xiàn)中改方法是始終返回false的:
- protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
- return false;
- }
而removeEldestEntry方法在每一次調(diào)用put或者putAll方法向Map中添加entry的時候都會被調(diào)用,通過該方法的返回值,判斷是否需要將最“老”的一個entry刪除。可見JDK為LinkedHashMap提供了一種靈活的控制Map大小的方法,而tomcat則利用了LinkedHashMap的這一特性。而后,將登陸失敗的用戶存儲在Map中,并通過記錄當前時間,在以后的登陸中判斷是否對當前用戶放行就很好實現(xiàn)了:
- private void registerAuthFailure(String username) {
- LockRecord lockRecord = null;
- synchronized (this) {
- if (!failedUsers.containsKey(username)) {
- lockRecord = new LockRecord();
- failedUsers.put(username, lockRecord); //第一次登錄失敗,加入黑名單
- } else {
- lockRecord = failedUsers.get(username);
- if (lockRecord.getFailures() >= failureCount &&
- ((System.currentTimeMillis() -
- lockRecord.getLastFailureTime())/1000)
- > lockOutTime) {
- // User was previously locked out but lockout has now
- // expired so reset failure count
- lockRecord.setFailures(0); //距離上次失敗時間久遠,重置失敗次數(shù)
- }
- }
- }
- lockRecord.registerFailure(); //失敗次數(shù)自增,失敗時間更新,用于下次判斷
- }
JAASRealm是Tomcat提供的最為開放的安全域,采用了JAAS規(guī)范相關的類和接口,因為JAAS安全域中進行實際認證的類需要用戶按照使用場景進行實現(xiàn),因此JAAS安全域也被稱為自定義安全域。
JAAS規(guī)范全稱為Java Authentication and Authorization Service,是一套可插拔的認證授權機制,Tomcat實現(xiàn)的現(xiàn)有安全域都可以通過JAAS安全域進行實現(xiàn)。JAAS安全域的認證流程如下:
- 使用當前配置創(chuàng)建一個LoginContext的實例,配置包括LoginModule的名稱,用于傳遞認證信息的JAASCallbackHandler實例,configFile配置。
- 通過LoginContext.login()方法進行驗證。
- 如果沒有異常且認證信息不為空,則認證成功;否則捕獲異常,認證失敗。關鍵代碼如下:
- protected Principal authenticate(String username,
- CallbackHandler callbackHandler) {
- ……
- try {
- Configuration config = getConfig();
- loginContext = new LoginContext(//構造LoginContext
- appName, null, callbackHandler, config);
- }
- ……
- try {
- loginContext.login();//調(diào)用login方法進行認證
- subject = loginContext.getSubject();//獲取認證信息,為空則認證失敗
- if (subject == null) {
- if( log.isDebugEnabled())
- log.debug(sm.getString("jaasRealm.failedLogin", username));
- return (null);
- }
- }
- ……
- }
這個流程是不是看起來超簡單?只需按自己的需求實現(xiàn)LoginModule,JAAS安全域的認證工作便如行云流水般了。LoginModule的實現(xiàn)可參照相關文檔或JDK中的源碼,默認JDK已經(jīng)提供了6種實現(xiàn)哦。
七、總結一下
本文重點介紹了Tomcat安全域部分的實現(xiàn),結合部署描述符web.xml中的配置,講解了Tomcat安全域?qū)φJ證、授權工作的流程處理。文章中對HTTP的四種認證方式進行了較大篇幅的講解,在授權部分也詳細講解了Tomcat的處理流程。最后在安全域?qū)崿F(xiàn)部分,重點介紹了兩種特殊的安全域,并簡單分析了JAAS規(guī)范的相關內(nèi)容,就是這樣啦。
【本文為51CTO專欄作者“侯樹成”的原創(chuàng)稿件,轉(zhuǎn)載請通過作者微信公眾號『Tomcat那些事兒』獲取授權】