更具體地說,它打算顯示如何在一個Web應用程序中配置兩個不同的安全領域。
第一安全領域是針對瀏覽器客戶端的。 它使我們能夠在登錄頁面中登錄并訪問受保護的資源。
第二安全領域旨在處理來自android應用程序的REST Web服務請求。 在每個請求上,REST客戶端應將所需的信息發送到服務器,并且此信息將用于確定是否應允許RESTfull請求通過。
這兩個安全領域(配置)的區別在于Web應用程序中資源的URL模式不同。 在這兩種配置中,我們都可以重用相同的身份驗證邏輯。
第一安全領域
我們有一個經典的Web應用程序,其中包含一些受保護的資源(頁面)。 為了訪問這些資源,用戶應在登錄頁面上登錄到應用程序。 如果登錄成功,則將用戶轉發到所請求的資源。 如果用戶的登錄過程由于某種原因(例如,錯誤的用戶名或密碼)而失敗,則該用戶將無法獲得受保護的資源,并且將其重定向到登錄頁面,并顯示相應的消息。
我在上一節中剛剛描述的情況可能被認為是“經典的Web應用程序行為”。 普通的互聯網用戶至少會遇到數百個這樣的在線應用程序。 這種行為旨在與客戶(瀏覽器)一起使用。 由于這種行為在當今非常普遍,因此Spring安全性使得實現它非常容易。 顯然,基于表單的身份驗證機制最適合我們。 在Spring Security中,當您希望定義與客戶端身份驗證狀態相關的操作時,可以定義入口點。 這是我們標準瀏覽器-客戶端入口點的預覽:
<http? entry-point-ref="loginUrlAuthenticationEntryPoint" use-expressions="true"><intercept-url pattern="/includes/content/administration.jsp" access="hasAnyRole('ROLE_100','ROLE_1000')" /><intercept-url pattern="/includes/content/userAdministration.jsp" access="hasAnyRole('ROLE_100','ROLE_1000')" /><intercept-url pattern="/includes/content/groupAdministration.jsp" access="hasAnyRole('ROLE_100','ROLE_1000')" /><intercept-url pattern="/includes/content/departmentAdministration.jsp" access="hasAnyRole('ROLE_100','ROLE_1000')" /><intercept-url pattern="/includes/content/shiftAdministration.jsp" access="hasAnyRole('ROLE_100','ROLE_101','ROLE_1000') /><custom-filter position="FORM_LOGIN_FILTER" ref="userAuthenticationProcessingFilter" /><logout logout-url='/logout' />
</http><beans:bean id="loginUrlAuthenticationEntryPoint"class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint"><beans:property name="loginFormUrl" value="/login.jsp" />
</beans:bean>
希望這是不言而喻的。 loginUrlAuthenticationEntryPoint是一個入口點,您可以在其中配置實現了登錄功能的登錄頁面。 然后,在http元素中,我們將該入口點的行為配置為更多詳細信息。 首先,我們定義了攔截URL元素列表。 僅當請求了這些資源之一時,才會激活此入口點。 我們還用我們自己的自定義版本替換了默認的FORM_LOGIN_FILTER 。 Spring安全性通過應用您在入口點中定義的過濾器鏈來發揮作用。 這些基本上是標準的servlet過濾器。 您可以使用Spring預定義過濾器,也可以擴展它們并插入自定義過濾器。 在這里,我們使用了Spring的安全過濾器之一。
這是一個UsernamePasswordAuthenticationFilter 。 它用于具有用戶名和密碼字段的登錄頁面的情況。 該過濾器使我們能夠結合將用于身份驗證的自定義機制。 它還允許我們定義在成功和不成功的身份驗證的情況下將要采取的措施。 讓我們看看這種配置如何:
<beans:bean id="loginSuccessHandler"class="org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler"><beans:property name="defaultTargetUrl" value="/main.jsp" />
</beans:bean><beans:bean id="userAuthenticationProcessingFilter"class="org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter"><beans:property name="authenticationManager" ref="authenticationManager" /><beans:property name="authenticationFailureHandler"ref="loginMappingFailureHandler" /><beans:property name="authenticationSuccessHandler"ref="loginSuccessHandler" />
</beans:bean><beans:bean id="loginMappingFailureHandler"class="org.springframework.security.web.authentication.ExceptionMappingAuthenticationFailureHandler"><beans:property name="exceptionMappings" ref="failureUrlMap" />
</beans:bean><util:map id="failureUrlMap" map-class="java.util.HashMap"><beans:entrykey="org.springframework.security.authentication.BadCredentialsException"value="/login.jsp?errorMessage=bad.credentials" /><beans:entrykey="org.springframework.security.authentication.DisabledException"value="/login.jsp?errorMessage=disabled.user" />
</util:map>
我們再來看一下此配置。 我將解釋我們在這里所做的事情。
首先,我們定義了表單登錄過濾器。 實際上,我們為此定義了三件事。 我們為它提供了自定義身份驗證機制,該機制將在整個應用程序中使用。 該機制通過authenticationManager插入到過濾器中。 我將很快談論認證管理器。
第二件事,我們定義了一個登錄失敗處理程序。 基本上,這是Spring異常以及對這些異常采取的措施的映射。 異常由AuthenticationProvider引發,如下所述。 例如,當用戶輸入錯誤的用戶名或密碼時,將引發BadCredentialsException 。 發生這種情況時,用戶將再次重定向到登錄頁面。 還將某些參數附加到登錄頁面的URL,以使我們能夠顯示正確的錯誤消息。
第三,也是最后一件事,我們定義了一個成功的身份驗證處理程序。 這確實很明顯。 我們正在定義如果登錄成功該怎么辦。 用戶被發送到主頁。
現在,讓我們來談談認證管理器。 這只是Spring使用的接口。 可以是任何東西。 它可以是用戶數據庫,LDAP服務器或其他數據庫。 身份驗證管理器不能自行完成工作。 它僅使用身份驗證提供程序為其執行實際的身份驗證工作。 身份驗證提供程序在被調用時可以做兩件事:
- 可以返回成功填充的對象(這是Spring的Authentication接口的實例)
- 可以拋出適當的Spring安全異常之一
身份驗證管理器配置如下所示:
<authentication-manager alias="authenticationManager"><authentication-provider ref="authenticationProvider" />
</authentication-manager><beans:bean id="authenticationProvider" class="ba.codecentric.medica.security.UserAuthenticationProvider"><beans:property name="userService" ref="userService"/><beans:property name="licenseInformationWrapper" ref="licenseInformationWrapper"/>
</beans:bean>
這是我的定制身份驗證提供程序的源代碼:
package ba.codecentric.medica.security;import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;import org.apache.commons.collections.CollectionUtils;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;import ba.codecentric.medica.administration.service.UserService;
import ba.codecentric.medica.model.Group;
import ba.codecentric.medica.model.LicenseInformationWrapper;
import ba.codecentric.medica.model.Role;
import ba.codecentric.medica.model.User;public class UserAuthenticationProvider implements AuthenticationProvider {private static final String ROLE_PREFIX = "ROLE_";private UserService userService;private LicenseInformationWrapper licenseInformationWrapper;@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {User user = userService.getUserByUsernameAndPassword(authentication.getName(), authentication.getCredentials().toString(), true);if (user != null) {Collection authorities = new ArrayList(buildRolesFromUser(user));authorities.addAll(getActivatedModulesAsRoles());return new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(),authorities);} else {throw new BadCredentialsException("Try again");}}private Collection getActivatedModulesAsRoles() {List activatedModules = new ArrayList();if(CollectionUtils.isNotEmpty(licenseInformationWrapper.getActivatedModules())) {for(String activatedModuleName: licenseInformationWrapper.getActivatedModules()) {activatedModules.add(new SimpleGrantedAuthority(ROLE_PREFIX + activatedModuleName));}}return activatedModules;}private Collection buildRolesFromUser(User user) {Collection authorities = new HashSet();for (Group group : user.getGroups()) {for (Role role : group.getRoles()) {authorities.add(new SimpleGrantedAuthority(ROLE_PREFIX + role.getName()));}}return authorities;}@Overridepublic boolean supports(Class authentication) {return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));}public UserService getUserService() {return userService;}public void setUserService(UserService userService) {this.userService = userService;}public LicenseInformationWrapper getLicenseInformationWrapper() {return licenseInformationWrapper;}public void setLicenseInformationWrapper(LicenseInformationWrapper licenseInformationWrapper) {this.licenseInformationWrapper = licenseInformationWrapper;}}
如您所見,身份驗證過程非常簡單。 我的自定義身份驗證提供程序實現了Spring AuthenticationProvider接口。
就像我們之前討論的那樣,它可以完成工作。 它在數據庫的用戶表中查找用戶名和密碼。 如果找到這樣的用戶,則創建并返回認證對象。 否則,如果未找到此類用戶,則authenticate方法將引發適當的異常。 還有一件事情。 Spring使用GrantedAuthority對象的集合來表示分配給用戶的角色。 這就是為什么我們構建這樣的集合并將其附加到身份驗證對象的原因。 必須將數據庫中與用戶連接的每個角色都添加到已授予權限的集合中,以使Spring將此角色視為角色。 并且每個角色必須具有ROLE_前綴。 我們還有另一件事要展示。 如何從登錄網頁調用此過濾器? 這是login.jsp的一部分:
<form id="loginForm" method="POST" action="j_spring_security_check"><table>
<tr><td><b><fmt:message key="login.username.label" />:</b></td><c:choose><c:when test="${not empty param.j_username}" ><td><input type="text" name="j_username" id="username" value="${param.j_username }" class="loginInput" /></td>??? </c:when><c:otherwise><td><input type="text" name="j_username" id="username" class="loginInput"/></td></c:otherwise></c:choose> </tr>
<tr><td><b><fmt:message key="login.password.label" />:</b></td><c:choose><c:when test="${not empty param.j_password}" ><td><input type="password" name="j_password" id="password" value="${param.j_password }" class="loginInput" /></td>??? </c:when><c:otherwise><td><input type="password" name="j_password" id="password" class="loginInput" /></td></c:otherwise></c:choose> </tr>
<tr><td><b><fmt:message key="login.alphabet.label" /></b>:</td><td><select id="alphabet" class="fullWidth" onchange="languageSelect();"><option value="lat"><fmt:message key="login.alphabet.lat" /></option><option value="cir"><fmt:message key="login.alphabet.cyr" /></option></select></td></tr>
<tr><td></td><td><input type="submit" value="<fmt:message key="login.button.label" />" class="right"></td></tr>
</table></form>
默認情況下,標準Spring安全性設置要求您通過調用j_spring_security_check從登錄表單中調用安全鏈。 用戶名和密碼過濾器將攔截此URL(默認設置),但您可以將其配置為攔截任何其他URL。 好吧,這一切都與“基于瀏覽器的客戶端”安全領域有關。 如果用戶未登錄并嘗試訪問受該領域(入口點)保護的資源,則該領域將重定向用戶到登錄頁面并要求他登錄。只有當用戶登錄時,才受保護資源將可用。
第二安全領域
現在最后,讓我們討論應用程序中的第二個安全領域。 我們僅在博客的簡介部分提到了它。 該應用程序支持REST服務調用。 我們必須實現將應用程序的某些部分與運行在移動設備上的簡單android應用程序同步的要求。 我們認為最簡單的方法是從手機到Web應用程序進行RESTfull調用。 當然,我們在這里也需要安全。 我們不想讓用戶始終能夠連接到應用程序。 用戶列表及其角色在數據庫中維護。 例如,某個用戶今天可以處于活動狀態,但是明天管理員可以決定該用戶不再處于活動狀態,并且不應該能夠連接到應用程序(也應該不能登錄)。 因此,必須在REST服務領域中實現安全性。
讓我們考慮一下這個領域。 這些REST調用應該如何工作。 Android應用程序將POST請求(RESTfull請求)發送到Web應用程序以獲取某些數據(醫生的約會等)。 應用程序查找并返回請求的數據。 然后,Android應用程序處理獲取的數據并將其顯示給用戶。
現在,我們將安全性添加到此RESTfull概念中,并嘗試用安全性描述概念。 Android應用程序發送POST請求。 Android應用程序發送一個包含哈希用戶名和密碼的標頭,作為每個請求的一部分(請參閱-> 基本身份驗證 )。
Web應用程序的安全領域(入口點)應該接收到此請求,并且如果用戶名和密碼確實是活動用戶,則允許此請求到達Web應用程序中的REST服務,并將對其進行處理。 如果用戶名和密碼無效(或用戶處于非活動狀態),則請求應在安全入口點失敗,這意味著我們應立即返回格式正確的HTTP響應,該響應將通知客戶端應用程序該用戶與該用戶名稱和密碼不允許訪問Web應用程序中的REST服務。
正如我們在這種情況下所看到的,先前定義的入口點的行為與REST服務不對應。 上一個入口點,如果用戶未通過身份驗證,則將其重定向到登錄頁面。 這意味著,如果用戶未通過身份驗證,則服務器實際上會返回包含登錄頁面HTML代碼的HTTP響應。 我們無法在android應用程序中處理這種行為,因為它不會顯示任何HTML網頁。 那么,當它收到登錄頁面HTML時會怎么做?
這就是為什么我們實際上需要Web應用程序中的第二個安全域(安全性入口點)的主要原因,該安全域的工作方式不同于處理瀏覽器客戶端的機制。 如果用戶無法通過身份驗證,此新的安全領域將僅將格式正確的HTTP響應返回給客戶端應用程序(它將在響應上設置特定的HTTP狀態和HTTP消息)。 我們知道,在Java Server環境中,我們有一種稱為基本身份驗證的安全性。 它基于發送散列的用戶名和密碼作為請求標頭的一部分(標頭必須以特定方式進行格式化)。 然后,如果可以在用戶數據池中找到用戶名和密碼,則允許通過請求。 否則,將返回HTTP響應以及相應的狀態和消息,以告知客戶端不允許其訪問某些資源。 對我們來說幸運的是,Spring支持這種身份驗證機制。 我們將添加另一個入口點和一個過濾器。 它將是這樣的:
<http entry-point-ref="basicAuthEntryPoint" pattern="/ws/**" use-expressions="true"><intercept-url pattern="/ws/schedule/patients" access="hasAnyRole('ROLE_1','ROLE_100','ROLE_300','ROLE_1000')" /><custom-filter ref="basicAuthenticationFilter" after="BASIC_AUTH_FILTER" />
</http><beans:bean id="basicAuthEntryPoint" class="org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint"><beans:property name="realmName" value="REST Realm" />
</beans:bean><beans:bean id="basicAuthenticationFilter" class="org.springframework.security.web.authentication.www.BasicAuthenticationFilter"><beans:property name="authenticationManager" ref="authenticationManager"/><beans:property name="authenticationEntryPoint" ref="basicAuthEntryPoint" />
</beans:bean>
基本上,我們添加了一個新的入口點(安全領域),該入口點攔截URL路徑/ ws / **上的所有請求。 這是我們所有REST服務調用通過的路徑。 我們使用了Springs BasicAuthenticationFilter ,它提供了讀取請求標頭和調用身份驗證管理器的功能。 如果在數據庫中找到了用戶名和密碼(由身份驗證管理器處理),將允許請求進一步傳遞。 如果在數據庫中找不到用戶名和密碼,入口點將在HTTP響應上設置狀態401,并立即將此響應返回給客戶端。 這只是我們Android應用程序所需的行為。
這就是我們的應用程序需要的所有安全配置。 現在剩下要做的就是在web.xml文件中啟用Spring安全過濾器。 我已經提到過,Spring安全性可以通過在請求上調用過濾器鏈來實現。 這意味著存在某種“主”過濾器,它是所有其他后續過濾器和服務的基礎。 在web.xml中啟用并配置了該“主”過濾器。 這是我的配置:
<filter><filter-name>springSecurityFilterChain</filter-name><filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping><filter-name>springSecurityFilterChain</filter-name><url-pattern>/*</url-pattern><dispatcher>ERROR</dispatcher><dispatcher>REQUEST</dispatcher><dispatcher>INCLUDE</dispatcher><dispatcher>FORWARD</dispatcher>
</filter-mapping>
如您所見,主要的Spring安全過濾器已配置為攔截對應用程序中所有資源的所有請求。 但是,哪些資源真正受到保護,哪些資源是公共資源,則由入口點控制(通過http元素中的URL模式)。 例如,位于/ css文件夾中的所有資源都被視為公用資源,不需要用戶進行身份驗證就可以訪問它們:
<http pattern="/css/**" security="none" />
另一方面,像管理頁面這樣的資源受到保護,并且如果用戶希望訪問該頁面,則不僅要求用戶進行身份驗證,還需要分配某些角色。 這是此xml代碼段中的示例:
<!-- more xml -->
<intercept-url pattern="/includes/content/administration.jsp" access="hasAnyRole('ROLE_100','ROLE_1000')" />
<!-- more xml -->
還有兩件事要說!
當您的安全配置中有多個http元素時,請確保具有最特定模式屬性的元素位于不那么特定甚至可能沒有模式屬性的元素之前。 否則,當Spring開始抱怨應用程序中的過濾器排序沒有意義時,您將在日志文件中看到很長的堆棧跟蹤。
閱讀此博客后,您可能會開始認為添加基于表單的身份驗證或基本身份驗證就足夠了,您的應用程序將是安全的。 但是,那不是完全正確的。 任何對HTTP協議和網絡具有“技術”知識的人都可以想到如何在網絡內部攔截HTTP數據流的方式。 對于基本身份驗證和基于表單的身份驗證,諸如用戶名和密碼之類的信息直接通過HTTP協議發送。 對于基本身份驗證,它們將作為HTTP請求標頭發送。 對于基于表單的身份驗證,它們將作為請求參數發送。 因此,可以攔截和讀取這些HTTP流的人員可以輕松讀取您的標頭并請求參數。 稍后,同一個人可以手動創建請求,并將那些標頭或參數附加到請求。 當然,此新請求現在將由容器授權,因為它包含正確的身份驗證詳細信息。
那么,我們該怎么做才能避免對應用程序的這些安全攻擊?
真正的答案是:我們應該在保護應用程序資源的地方使用HTTPS協議 。 僅通過使用HTTPS協議和Java服務器的身份驗證機制,我們就可以肯定地說我們的應用程序確實是安全的。
參考: Spring Security –我們的JCG合作伙伴 Branislav Vidovi提供了一個應用程序中的兩個安全領域 ? 在極客的東西:-)博客上。
翻譯自: https://www.javacodegeeks.com/2012/08/spring-security-two-security-realms-in.html