Spring Security –在一个应用程序中有两个安全领域

这篇博客文章主要是关于Spring Security配置的。
更具体地说,它打算显示如何在一个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:entry
  key="org.springframework.security.authentication.BadCredentialsException"
  value="/login.jsp?errorMessage=bad.credentials" />
 <beans:entry
  key="org.springframework.security.authentication.DisabledException"
  value="/login.jsp?errorMessage=disabled.user" />
</util:map>

我们再来看一下此配置。 我将解释我们在这里所做的事情。
首先,我们定义了表单登录过滤器。 实际上,我们为此定义了三件事。 我们为它提供了自定义身份验证机制,该机制将在整个应用程序中使用。 该机制通过authenticationManager插入到过滤器中。 我将很快谈论认证管理器。

第二件事,我们定义了一个登录失败处理程序。 基本上,这是Spring异常以及对这些异常采取的措施的映射。 异常由AuthenticationProvider引发,如下所述。 例如,当用户输入错误的用户名或密码时,将引发BadCredentialsException 。 发生这种情况时,用户将再次重定向到登录页面。 还将某些参数附加到登录页面的URL,以使我们能够显示正确的错误消息。

第三,也是最后一件事,我们定义了一个成功的身份验证处理程序。 这确实很明显。 我们正在定义如果登录成功该怎么办。 用户被发送到主页。

现在,让我们来谈谈认证管理器。 这只是Spring使用的接口。 可以是任何东西。 它可以是用户数据库,LDAP服务器或其他数据库。 身份验证管理器不能自行完成工作。 它仅使用身份验证提供程序为其执行实际的身份验证工作。 身份验证提供程序在被调用时可以做两件事:

  1. 可以返回成功填充的对象(这是Spring的Authentication接口的实例)
  2. 可以抛出适当的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;
 
 @Override
 public 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;
 }
 
 @Override
 public 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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值