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