vlambda博客
学习文章列表

读书笔记《spring-security-third-edition》自定义身份验证

自定义身份验证

第 2 章中,Spring Security 入门,我们演示了如何使用内存数据存储来验证用户。在本章中,我们将探索如何通过扩展 Spring Security 的身份验证支持以使用我们现有的 API 集来解决一些常见的现实问题。通过这次探索,我们将了解 Spring Security 用于对用户进行身份验证的每个构建块。

在本章的课程中,我们将讨论以下主题:

  • Leverage Spring Security’s annotations and Java-based configuration
  • Discovering how to obtain the details of the currently logged-in user
  • Adding the ability to log in after creating a new account
  • Learning the simplest method for indicating to Spring Security, that a user is authenticated
  • Creating custom UserDetailsService and AuthenticationProvider implementations that properly decouple the rest of the application from Spring Security
  • Adding domain-based authentication to demonstrate how to authenticate with more than just a username and password

JBCP 日历架构

第 1 章中,剖析不安全的应用程序,以及第 2 章Spring Security 入门< /em>,我们使用 Spring IO BOM 来辅助依赖管理,但是项目中的其余代码使用的是核心 Spring Framework,需要手动配置。从本章开始,我们将对其余应用程序使用 Spring Boot,以简化应用程序配置过程。我们将为 Spring Boot 和非 Boot 应用程序创建的 Spring Security 配置将是相同的。我们将在 附录 中介绍有关 Spring IO 和 Spring Boot 的更多详细信息, 其他参考资料

由于本章是关于将 Spring Security 与自定义用户和 API 集成,我们将从快速介绍 JBCP 日历应用程序中的域模型开始。

CalendarUser 对象

我们的日历应用程序使用一个名为 CalendarUser 的域对象,其中包含有关我们用户的信息,如下所示:

    //src/main/java/com/packtpub/springsecurity/domain/CalendarUser.java

    public class CalendarUser implements Serializable {
       private Integer id;
       private String firstName;
       private String lastName;
       private String email;
       private String password;
       ... accessor methods omitted ..
    }

事件对象

我们的应用程序有一个 Event 对象,其中包含有关每个事件的信息,如下所示:

    //src/main/java/com/packtpub/springsecurity/domain/Event.java

    public class Event {
       private Integer id;
       private String summary;
       private String description;
       private Calendar when;
       private CalendarUser owner;
       private CalendarUser attendee;
       ... accessor methods omitted ..
    }

日历服务接口

我们的应用程序包含一个 CalendarService 接口,可用于访问和存储我们的域对象。 CalendarService 的代码如下:

    //src/main/java/com/packtpub/springsecurity/service/CalendarService.java

    public interface CalendarService {
       CalendarUser getUser(int id);
       CalendarUser findUserByEmail(String email);
       List<CalendarUser> findUsersByEmail(String partialEmail);
       int createUser(CalendarUser user);
       Event getEvent(int eventId);
       int createEvent(Event event);
       List<Event> findForUser(int userId);
       List<Event> getEvents();
    }

我们不会详细介绍 CalendarService 中使用的方法,但它们应该相当简单。如果您想详细了解每个方法的作用,请参阅示例代码中的 Javadoc。

用户上下文接口

像大多数应用程序一样,我们的应用程序需要我们与当前登录的用户进行交互。我们创建了一个非常简单的接口,叫做 UserContext 来管理当前登录的用户,如下所示:

    //src/main/java/com/packtpub/springsecurity/service/UserContext.java

    public interface UserContext {
       CalendarUser getCurrentUser();
       void setCurrentUser(CalendarUser user);
    }

这意味着我们的应用程序可以调用 UserContext.getCurrentUser() 来获取当前登录用户的详细信息。它还可以调用 UserContext.setCurrentUser(CalendarUser) 来指定登录的用户。在本章后面,我们将探讨如何编写这个接口的实现,使用 Spring Security 来访问我们当前的用户并使用 SecurityContextHolder 获取他们的详细信息。

Spring Security 提供了很多不同的方法来验证用户。然而,最终结果是 Spring Security 将使用 o.s.s.core.Authentication 填充 o.s.s.core.context.SecurityContextAuthentication 对象表示我们在身份验证时收集的所有信息(用户名、密码、角色等)。然后在 o.s.s.core.context.SecurityContextHolder 接口上设置 SecurityContext 接口。这意味着 Spring Security 和开发人员可以使用 SecurityContextHolder 来获取有关当前登录用户的信息。获取当前用户名的示例如下:

    String username = SecurityContextHolder.getContext()
       .getAuthentication()
       .getName();
应该注意 null 检查应该始终对 Authentication 对象进行,因为如果用户不是,这可能是 null已登录。

SpringSecurityUserContext 接口

当前的 UserContext 实现 UserContextStub 是一个始终返回相同用户的存根。这意味着 My Events 页面将始终显示相同的用户,无论谁登录。让我们更新我们的应用程序以利用当前 Spring Security 用户的用户名,以确定哪些事件要显示在 我的活动 页面上。

您应该从 chapter03.00-calendar 中的示例代码开始。

请看以下步骤:

  1. The first step is to comment out the @Component attribute on UserContextStub, so that our application no longer uses our scanned results.
@Component 注释与 @ComponentScan 注释在 com/packtpub/springsecurity/web/configuration/WebMvcConfig.java,自动创建 Spring bean,而不是为每个 bean 创建显式 XML 或 Java 配置。您可以在 Spring Reference 链接中了解有关 Spring 扫描的类路径的更多信息 http://static.springsource.org/spring/docs/当前/spring-framework-reference/html/

看看下面的代码片段:

        //src/main/java/com/packtpub/springsecurity/service/UserContextStub.java

        ...
        //@Component
        public class UserContextStub implements UserContext {
        ...
  1. The next step is to utilize SecurityContext to obtain the currently logged-in user. We have included SpringSecurityUserContext within this chapter's code, which is wired up with the necessary dependencies but contains no actual functionality.
  2. Open the SpringSecurityUserContext.java file and add the @Component annotation. Next, replace the getCurrentUser implementation, as illustrated in the following code snippet:
        //src/main/java/com/packtpub/springsecurity/service/
        SpringSecurityUserContext.java

        @Component
        public class SpringSecurityUserContext implements UserContext {
          private final CalendarService calendarService;
          private final UserDetailsService userDetailsService;
        @Autowired
        public SpringSecurityUserContext(CalendarService calendarService, 
        UserDetailsService userDetailsService) {
           this.calendarService = calendarService;
           this.userDetailsService = userDetailsService;
        }
        public CalendarUser getCurrentUser() {
           SecurityContext context = SecurityContextHolder.getContext();
           Authentication authentication = context.getAuthentication();
           if (authentication == null) {
             return null;
           }
           String email = authentication.getName();
           return calendarService.findUserByEmail(email);
        }
        public void setCurrentUser(CalendarUser user) {
           throw new UnsupportedOperationException();
        }
        }

我们的代码从当前 Spring Security Authentication 对象中获取用户名,并利用该用户名通过电子邮件地址查找当前 CalendarUser 对象。由于我们的 Spring Security 用户名是一个电子邮件地址,我们可以使用该电子邮件地址将 CalendarUser 与 Spring Security 用户链接。请注意,如果我们要链接帐户,我们通常希望使用我们生成的密钥来执行此操作,而不是可能会更改的内容(即电子邮件地址)。我们遵循仅将我们的域对象返回给应用程序的良好做法。这确保我们的应用程序只知道我们的 CalendarUser 对象,因此不与 Spring Security 耦合。

这段代码看起来与我们在
标记属性时非常相似application-development/9781787129511/2" linkend="ch02lvl1sec07">第 2 章
Spring Security 入门,显示当前用户的用户名。实际上,Spring Security 标记库使用 SecurityContextHolder 的方式与我们在这里所做的相同。我们可以使用我们的 UserContext 接口将当前用户放在 HttpServletRequest 上,从而消除我们对 Spring Security 标记库的依赖。

  1. Start up the application, visit http://localhost:8080/, and log in with [email protected] as the username and admin1 as the password.
  2. Visit the My Events page, and you will see that only the events for that current user, who is the owner or the attendee, are displayed.
  3. Try creating a new event; you will observe that the owner of the event is now associated with the logged-in user.
  4. Log out of the application and repeat these steps with [email protected] as the username and user1 as the password.
您的代码现在应该类似于 chapter03.01-calendar

使用 SecurityContextHolder 登录新用户

一个常见的要求是允许用户创建一个新帐户,然后自动将他们登录到应用程序。在本节中,我们将描述通过利用 SecurityContextHolder 来指示用户已通过身份验证的最简单方法。

在 Spring Security 中管理用户

第 1 章中提供的应用程序,不安全应用程序剖析 提供了一种创建新 CalendarUser 对象的机制,因此在用户注册后创建我们的 CalendarUser 对象应该相当简单。但是,Spring Security 不知道 CalendarUser。这意味着我们也需要在 Spring Security 中添加一个新用户。不用担心,本章稍后我们将不再需要对用户进行双重维护。

Spring Security 提供了一个 o.s.s.provisioning.UserDetailsManager 接口来管理用户。还记得我们在内存中的 Spring Security 配置吗?

    auth.inMemoryAuthentication().
    withUser("user").password("user").roles("USER");

.inMemoryAuthentication() 方法创建 UserDetailsManager 的内存实现,命名为 o.s.s.provisioning.InMemoryUserDetailsManager,可用于创建新的弹簧安全用户。

在 Spring Security 中从 XML 配置转换为基于 Java 的配置时,Spring Security DSL 目前存在一个限制,即当前不支持公开多个 bean。在此问题上打开了一个 JIRA https://jira.spring.io/browse/SPR-13779。

让我们看看我们如何通过执行以下步骤在 Spring Security 中管理用户:

  1. In order to expose UserDetailsManager using a Java-based configuration, we need to create InMemoryUserDetailsManager outside of the WebSecurityConfigurerAdapter DSL:
        //src/main/java/com/packtpub/springsecurity/configuration/
        SecurityConfig.java

        @Bean
        @Override
        public UserDetailsManager userDetailsService() {
           InMemoryUserDetailsManager manager = new 
           InMemoryUserDetailsManager();
           manager.createUser(
               User.withUsername("[email protected]")
                   .password("user1").roles("USER").build());
           manager.createUser(
               User.withUsername("[email protected]")
                   .password("admin1").roles("USER", "ADMIN").build());
           return manager;
        }
  1. Once we have an exposed UserDetailsManager interface in our Spring configuration, all we need to do is update our existing CalendarService implementation, DefaultCalendarService, to add a user in Spring Security. Make the following updates to the DefaultCalendarService.java file:
        //src/main/java/com/packtpub/springsecurity/service/
        DefaultCalendarService.java

        public int createUser(CalendarUser user) {
            List<GrantedAuthority> authorities = AuthorityUtils.
            createAuthorityList("ROLE_USER");
            UserDetails userDetails = new User(user.getEmail(),
            user.getPassword(), authorities);
           // create a Spring Security user
           userDetailsManager.createUser(userDetails);
           // create a CalendarUser
           return userDao.createUser(user);
        }
  1. In order to leverage UserDetailsManager, we first convert CalendarUser into the UserDetails object of Spring Security.

  1. Later, we use UserDetailsManager to save the UserDetails object. The conversion is necessary because Spring Security has no understanding of how to save our custom CalendarUser object, so we must map CalendarUser to an object Spring Security understands. You will notice that the GrantedAuthority object corresponds to the authorities attribute of our SecurityConfig file. We hardcode this for simplicity and due to the fact that there is no concept of roles in our existing system.

将新用户登录到应用程序

现在我们能够向系统添加新用户,我们需要表明用户已通过身份验证。更新SpringSecurityUserContext,在Spring Security的SecurityContextHolder对象上设置当前用户,如下:

    //src/main/java/com/packtpub/springsecurity/service/
    SpringSecurityUserContext.java

    public void setCurrentUser(CalendarUser user) {
      UserDetails userDetails = userDetailsService.
      loadUserByUsername(user.getEmail());
      Authentication authentication = new   
      UsernamePasswordAuthenticationToken(userDetails, user.getPassword(),
      userDetails.getAuthorities());
      SecurityContextHolder.getContext().
      setAuthentication(authentication);
    }

我们执行的第一步是将我们的 CalendarUser 对象转换为 Spring Security 的 UserDetails 对象。这是必要的,因为正如 Spring Security 不知道如何保存我们的自定义 CalendarUser 对象一样,Spring Security 也不了解如何使用我们的自定义 CalendarUser 做出安全决策目的。我们使用 Spring Security 的 o.s.s.core.userdetails.UserDetailsS​​ervice 接口来获取与 UserDetailsManager 保存的相同的 UserDetails 对象。 UserDetailsS​​ervice 接口提供了我们已经看到的 Spring Security 的 UserDetailsManager 对象提供的功能的子集,按用户名查找。

接下来,我们创建一个 UsernamePasswordAuthenticationToken 对象并将 UserDetails、密码和 GrantedAuthority 放入其中。最后,我们在 SecurityContextHolder 上设置身份验证。在 Web 应用程序中,Spring Security 会自动为我们将 SecurityContextHolder 中的 SecurityContext 对象关联到我们的 HTTP 会话。

重要的是不能指示 Spring Security 忽略 URL(即,使用 permitAll() 方法),如在 第二章Spring Security 入门,其中 SecurityContextHolder 被访问或设置。这是因为 Spring Security 将忽略该请求,因此不会持续存在 SecurityContext 用于后续请求。允许访问其中的 URL 的正确方法 SecurityContextHolder 用于指定 access 属性 antMatchers() 方法(即 antMatchers(…).permitAll())。

值得一提的是,我们可以通过直接创建一个新的 o.s.s.core.userdetails.User 对象来转换 CalendarUser,而不是在 UserDetailsS​​ervice。例如,以下代码还将验证用户:

List<GrantedAuthority> authorities =
AuthorityUtils.createAuthorityList("ROLE_USER");
UserDetails userDetails = new User("username","password",authorities);
Authentication authentication = new UsernamePasswordAuthenticationToken ( userDetails,userDetails.getPassword(),userDetails.getAuthorities());
SecurityContextHolder.getContext()
.setAuthentication(authentication);

这种方法的优点是无需再次访问数据存储。在我们的例子中,数据存储是内存中的数据存储,但这可以由数据库支持,这可能会产生一些安全隐患。这种方法的缺点是我们不能重用代码。由于这个方法很少被调用,我们选择重用代码。一般来说,最好分别评估每种情况以确定哪种方法最有意义。

更新注册控制器

应用程序有一个 SignupController 对象,它处理 HTTP 请求以创建一个新的 CalendarUser 对象。最后一步是更新 SignupController 以创建我们的用户,然后指示他们已登录。对 SignupController 进行以下更新:

//src/main/java/com/packtpub/springsecurity/web/controllers/
SignupController.java

@RequestMapping(value="/signup/new", method=RequestMethod.POST)
public String signup(@Valid SignupForm signupForm,
BindingResult result, RedirectAttributes redirectAttributes) {
... existing validation …
user.setPassword(signupForm.getPassword());
int id = calendarService.createUser(user);
user.setId(id);
userContext.setCurrentUser(user);
redirectAttributes.addFlashAttribute("message", "Success");
return "redirect:/";
}

如果您还没有这样做,请重新启动应用程序,访问 http://localhost:8080/,创建一个新用户,并看到新用户自动登录。

您的代码现在应该看起来像 chapter03.02-日历

创建自定义 UserDetailsS​​ervice 对象

虽然我们能够将我们的域模型 (CalendarUser) 与 Spring Security 的域模型 (UserDetails) 联系起来,但我们必须维护用户的多个表示。为了解决这种双重维护,我们可以实现一个自定义的 UserDetailsS​​ervice 对象,将我们现有的 CalendarUser 域模型转换为 Spring Security 的 UserDetails 接口的实现。通过将我们的 CalendarUser 对象转换为 UserDetails,Spring Security 可以使用我们的自定义域模型做出安全决策。这意味着我们将不再需要管理用户的两种不同表示。

CalendarUserDetailsS​​ervice 类

到目前为止,我们需要两种不同的用户表示:一种用于 Spring Security 做出安全决策,另一种用于我们的应用程序将我们的域对象关联到。创建一个名为 CalendarUserDetailsS​​ervice 的新类,这将使 Spring Security 知道我们的 CalendarUser 对象。这将确保 Spring Security 可以根据我们的域模型做出决策。创建一个名为 CalendarUserDetailsS​​ervice.java 的新文件,如下:

//src/main/java/com/packtpub/springsecurity/core/userdetails/
CalendarUserDetailsService.java

// imports and package declaration omitted

@Component
public class CalendarUserDetailsService implements
UserDetailsService {
private final CalendarUserDao calendarUserDao;
@Autowired
public CalendarUserDetailsService(CalendarUserDao
   calendarUserDao) {
   this.calendarUserDao = calendarUserDao;
}
public UserDetails loadUserByUsername(String username) throws
   UsernameNotFoundException {
   CalendarUser user = calendarUserDao.findUserByEmail(username);
  if (user == null) {
     throw new UsernameNotFoundException("Invalid
       username/password.");
   }
   Collection<? extends GrantedAuthority> authorities =
     CalendarUserAuthorityUtils.createAuthorities(user);
   return new User(user.getEmail(), user.getPassword(),
     authorities);
}
}
在 Spring Tool Suite 中,您可以使用 Shift+ Ctrl+ O 轻松添加缺少的导入。或者,您可以从下一个检查点复制代码( chapter03.03-日历)。

在这里,我们利用CalendarUserDao通过电子邮件地址获取CalendarUser。我们注意不要返回 null 值;相反,应该抛出 UsernameNotFoundException 异常,因为返回 null 会破坏 UserDetailsS​​ervice 接口。

然后我们将 CalendarUser 转换为由用户实现的 UserDetails,就像我们在前面的部分中所做的那样。

我们现在使用示例代码中提供的名为 CalendarUserAuthorityUtils 的实用程序类。这将基于电子邮件地址创建 GrantedAuthority,以便我们可以支持用户和管理员。如果电子邮件以 admin 开头,则用户被视为 ROLE_ADMIN, ROLE_USER。否则,用户将被视为 ROLE_USER。当然,我们不会在实际应用程序中这样做,但正是这种简单性让我们能够专注于这一课。

配置 UserDetailsS​​ervice

现在我们有了一个新的 UserDetailsS​​ervice 对象,让我们更新 Spring Security 配置以利用它。我们的 CalendarUserDetailsS​​ervice 类会自动添加到 Spring 配置中,因为我们利用了类路径扫描和 @Component 注释。这意味着我们只需要更新 Spring Security 以引用我们刚刚创建的 CalendarUserDetailsS​​ervice 类。我们还能够删除 configure()userDetailsS​​ervice() 方法,这是 Spring Security 的 UserDetailsS​​ervice 的内存实现,因为我们现在提供我们自己的 UserDetailsS​​ervice 实现。更新 SecurityConfig.java 文件,如下:

//src/main/java/com/packtpub/springsecurity/configuration/SecurityConfig.java

@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
    ...
}
@Bean
@Override
public UserDetailsManager userDetailsService() {
    ...
}

删除对 UserDetailsManager 的引用

我们需要删除我们在 DefaultCalendarService 中添加的代码,该代码使用 UserDetailsManager 来同步 Spring Security o.s.s.core.userdetails.User 接口和 日历用户。首先,代码不是必需的,因为 Spring Security 现在引用 CalendarUserDetailsS​​ervice。其次,由于我们删除了 inMemoryAuthentication() 方法,我们的 Spring 配置中没有定义 UserDetailsManager 对象。继续并删除在 DefaultCalendarService 中找到的对 UserDetailsManager 的所有引用。更新将类似于以下示例片段:

//src/main/java/com/packtpub/springsecurity/service/
DefaultCalendarService.java

public class DefaultCalendarService implements CalendarService {
   private final EventDao eventDao;
   private final CalendarUserDao userDao;
   @Autowired
   public DefaultCalendarService(EventDao eventDao,CalendarUserDao userDao) {
       this.eventDao = eventDao;
       this.userDao = userDao;
   }
   ...
   public int createUser(CalendarUser user) {
       return userDao.createUser(user);
   }
}

启动应用程序并看到 Spring Security 的内存中 UserDetailsManager 对象不再需要(我们从 SecurityConfig.java 文件中删除了它)。

您的代码现在应该看起来像 chapter03.03-日历

CalendarUserDetails 对象

我们成功地消除了管理 Spring Security 用户和我们的 CalendarUser 对象的需要。但是,我们仍然需要不断地在两个对象之间进行转换,这仍然很麻烦。相反,我们将创建一个 CalendarUserDetails 对象,它可以被称为 UserDetailsCalendarUser。更新 CalendarUserDetailsS​​ervice 以使用 CalendarUserDetails,如下:

//src/main/java/com/packtpub/springsecurity/core/userdetails/
CalendarUserDetailsService.java

public UserDetails loadUserByUsername(String username) throws
UsernameNotFoundException {
...
return new CalendarUserDetails(user);
}
private final class CalendarUserDetails extends CalendarUser 
implements UserDetails {
CalendarUserDetails(CalendarUser user) {
   setId(user.getId());
   setEmail(user.getEmail());
   setFirstName(user.getFirstName());
   setLastName(user.getLastName());
   setPassword(user.getPassword());
}
public Collection<? extends GrantedAuthority>
   getAuthorities() {
   return CalendarUserAuthorityUtils.createAuthorities(this);
}
public String getUsername() {
   return getEmail();
}
public boolean isAccountNonExpired() { return true; }
public boolean isAccountNonLocked() { return true; }
public boolean isCredentialsNonExpired() { return true; }
public boolean isEnabled() { return true; }
}

在下一节中,我们将看到我们的应用程序现在可以引用当前 CalendarUser 对象上的主体身份验证。但是,Spring Security 可以继续将 CalendarUserDetails 视为 UserDetails 对象。

SpringSecurityUserContext 简化

我们更新了 CalendarUserDetailsS​​ervice 以返回一个 UserDetails 对象,该对象扩展了 CalendarUser 并实现了 UserDetails。这意味着,不必在两个对象之间进行转换,我们可以简单地引用一个 CalendarUser 对象。更新 SpringSecurityUserContext 如下:

public class SpringSecurityUserContext implements UserContext {
public CalendarUser getCurrentUser() {
   SecurityContext context = SecurityContextHolder.getContext();
   Authentication authentication = context.getAuthentication();
   if(authentication == null) {
      return null;
   }
   return (CalendarUser) authentication.getPrincipal();
}

public void setCurrentUser(CalendarUser user) {
   Collection authorities =
     CalendarUserAuthorityUtils.createAuthorities(user);
   Authentication authentication = new      UsernamePasswordAuthenticationToken(user,user.getPassword(), authorities);
   SecurityContextHolder.getContext()
     .setAuthentication(authentication);
}
}

更新不再需要使用 CalendarUserDao 或 Spring Security 的 UserDetailsS​​ervice 接口。还记得上一节中的 loadUserByUsername 方法吗?此方法调用的结果成为身份验证的主体。由于我们更新的 loadUserByUsername 方法返回一个扩展 CalendarUser 的对象,我们可以安全地将 Authentication 对象的主体转换为 CalendarUser 。在调用 setCurrentUser 方法时,我们可以将 CalendarUser 对象作为主体传递给 UsernamePasswordAuthenticationToken 的构造函数。这允许我们在调用 getCurrentUser 方法时仍然将主体转换为 CalendarUser 对象。

显示自定义用户属性

现在 CalendarUser 已填充到 Spring Security 的身份验证中,我们可以更新我们的 UI 以显示当前用户的名称而不是电子邮件地址。使用以下代码更新 header.html 文件:

    //src/main/resources/templates/fragments/header.html

    <ul class="nav navbar-nav pull-right" sec:authorize="isAuthenticated()">
       <li id="greeting">
           <p class="navbar-text">Welcome <div class="navbar-text" th:text="${#authentication.getPrincipal().getName()}">
           User</div></p>
       </li>

在内部,"${#authentication.getPrincipal().getName()}" 标签属性执行以下代码。观察突出显示的值与我们在 header.html 文件中指定的身份验证标记的 property 属性相关:

    SecurityContext context = SecurityContextHolder.getContext();
    Authentication authentication = context.getAuthentication();
    CalendarUser user = (CalendarUser) authentication.getPrincipal();
    String firstAndLastName = user.getName();

重启应用,访问http://localhost:8080/,登录查看更新。您现在应该看到他们的名字和姓氏,而不是看到当前用户的电子邮件。

您的代码现在应该类似于 chapter03.04-calendar

创建自定义 AuthenticationProvider 对象

Spring Security 委托给 AuthenticationProvider 对象来确定用户是否经过身份验证。这意味着我们可以编写自定义的 AuthenticationProvider 实现来告知 Spring Security 如何以不同的方式进行身份验证。好消息是 Spring Security 提供了很多 AuthenticationProvider 对象,因此您通常不需要创建一个。事实上,到目前为止,我们一直在使用 Spring Security 的 o.s.s.authentication.dao.DaoAuthenticationProvider 对象,它比较 UserDetailsS​​ervice 返回的用户名和密码。

CalendarUserAuthenticationProvider

在本节的其余部分,我们将创建一个名为 CalendarUserAuthenticationProvider 的自定义 AuthenticationProvider 对象,它将替换 CalendarUserDetailsS​​ervice。然后,我们将使用 CalendarUserAuthenticationProvider 来考虑一个额外的参数来支持对来自多个域的用户进行身份验证。

我们必须使用一个 AuthenticationProvider 对象而不是 UserDetailsS​​ervice,因为 UserDetails 接口没有域参数的概念。

新建一个名为CalendarUserAuthenticationProvider的类,如下:

    //src/main/java/com/packtpub/springsecurity/authentication/
    CalendarUserAuthenticationProvider.java

    // … imports omitted ...

    @Component
    public class CalendarUserAuthenticationProvider implements
    AuthenticationProvider {
    private final CalendarService calendarService;
    @Autowired
    public CalendarUserAuthenticationProvider
    (CalendarService    calendarService) {
       this.calendarService = calendarService;
    }
    public Authentication authenticate(Authentication
       authentication) throws AuthenticationException {
           UsernamePasswordAuthenticationToken token =   
           (UsernamePasswordAuthenticationToken) 
       authentication;
       String email = token.getName();
       CalendarUser user = null;
       if(email != null) {
         user = calendarService.findUserByEmail(email);
       }
       if(user == null) {
         throw new UsernameNotFoundException("Invalid
         username/password");
       }
       String password = user.getPassword();
       if(!password.equals(token.getCredentials())) {
         throw new BadCredentialsException("Invalid
         username/password");
       }
       Collection<? extends GrantedAuthority> authorities = CalendarUserAuthorityUtils.createAuthorities(user); return new UsernamePasswordAuthenticationToken(user, password, authorities); } public boolean supports(Class<?> authentication) {
       return UsernamePasswordAuthenticationToken
         .class.equals(authentication);
     }
    }
请记住,您可以使用 Shift+ Ctrl+ O 在 Eclipse 中轻松添加缺少的导入。或者,您可以从 chapter03.05-日历

在 Spring Security 可以调用 authenticate 方法之前,supports 方法必须为 Authentication 类返回 true传入。在这种情况下,AuthenticationProvider 可以验证用户名和密码。我们不接受 UsernamePasswordAuthenticationToken 的子类,因为可能存在我们不知道如何验证的其他字段。

authenticate 方法接受 Authentication 对象作为表示身份验证请求的参数。实际上,我们需要尝试验证的是来自用户的输入。如果身份验证失败,该方法应抛出 o.s.s.core.AuthenticationException 异常。如果身份验证成功,它应该返回一个 Authentication 对象,其中包含用户正确的 GrantedAuthority 对象。返回的 Authentication 对象将设置在 SecurityContextHolder 上。如果无法确定身份验证,则该方法应返回 null

验证请求的第一步是从 Authentication 对象中提取我们需要验证用户的信息。在我们的例子中,我们提取用户名并通过电子邮件地址查找 CalendarUser,就像 CalendarUserDetailsS​​ervice 一样。如果提供的用户名和密码匹配 CalendarUser,我们将返回一个带有正确 GrantedAuthorityUsernamePasswordAuthenticationToken 对象。否则,我们将抛出 AuthenticationException 异常。

还记得登录页面如何利用 SPRING_SECURITY_LAST_EXCEPTION 来解释登录失败的原因吗? AuthenticationProvider 中抛出的 AuthenticationException 异常的消息是最后一个 AuthenticationException 异常,如果登录失败,将显示在我们的登录页面上.

配置 CalendarUserAuthenticationProvider 对象

让我们执行以下步骤来配置 CalendarUserAuthenticationProvider

  1. Update the SecurityConfig.java file to refer to our newly created CalendarUserAuthenticationProvider object, and remove the reference to CalendarUserDetailsService, as shown in the following code snippet:
        //src/main/java/com/packtpub/springsecurity/configuration/
        SecurityConfig.java

        @Autowired CalendarUserAuthenticationProvider cuap; 
        @Override
        public void configure(AuthenticationManagerBuilder auth) 
        throws Exception {
           auth.authenticationProvider(cuap);
        }
  1. Restart the application and ensure everything is still working. As a user, we do not notice anything different. However, as a developer, we know that CalendarUserDetails is no longer required; we are still able to display the current user's first and last names, and Spring Security is still able to leverage CalendarUser for authentication.
您的代码现在应该看起来像 chapter03.05-日历

使用不同的参数进行身份验证

AuthenticationProvider 的优势之一是它可以使用您希望的任何参数进行身份验证。例如,您的应用程序可能使用随机标识符进行身份验证,或者它可能是一个多租户应用程序并且需要用户名、密码和域。在下一节中,我们将更新 CalendarUserAuthenticationProvider 以支持多个域。

域是确定用户范围的一种方式。例如,如果我们部署我们的应用程序一次,但有多个客户端使用相同的部署,每个客户端可能想要一个用户名 管理员。通过向我们的用户对象添加一个域,我们可以确保每个用户都是不同的并且仍然支持这个要求。

DomainUsernamePasswordAuthenticationToken 类

当用户进行身份验证时,Spring Security 使用用户提供的信息向 AuthenticationProvider 提交一个 Authentication 对象。当前的 UsernamePasswordAuthentication 对象仅包含用户名和密码字段。创建一个包含 domain 字段的 DomainUsernamePasswordAuthenticationToken 对象,如以下代码片段所示:

    //src/main/java/com/packtpub/springsecurity/authentication/
    DomainUsernamePasswordAuthenticationToken.java

    public final class DomainUsernamePasswordAuthenticationToken extends     
    UsernamePasswordAuthenticationToken {
            private final String domain;
            // used for attempting authentication
           public DomainUsernamePasswordAuthenticationToken(String
           principal, String credentials, String domain) {
              super(principal, credentials);
              this.domain = domain;
            } 
    // used for returning to Spring Security after being
    //authenticated
    public DomainUsernamePasswordAuthenticationToken(CalendarUser
       principal, String credentials, String domain,
       Collection<? extends GrantedAuthority> authorities) {
         super(principal, credentials, authorities);
         this.domain = domain;
       }
    public String getDomain() {
       return domain;
    }
    }

更新 CalendarUserAuthenticationProvider

让我们看一下更新 CalendarUserAuthenticationProvider.java 文件的以下步骤:

  1. Now, we need to update CalendarUserAuthenticationProvider to utilize the domain field as follows:
        //src/main/java/com/packtpub/springsecurity/authentication/
        CalendarUserAuthenticationProvider.java

        public Authentication authenticate(Authentication authentication) 
        throws AuthenticationException {
             DomainUsernamePasswordAuthenticationToken token =
             (DomainUsernamePasswordAuthenticationToken) authentication;
        String userName = token.getName();
        String domain = token.getDomain();
        String email = userName + "@" + domain;
        ... previous validation of the user and password ...
        return new DomainUsernamePasswordAuthenticationToken(user,
        password, domain, authorities);
        }
        public boolean supports(Class<?> authentication) {
          return DomainUsernamePasswordAuthenticationToken
          .class.equals(authentication);
        }
  1. We first update the supports method so that Spring Security will pass DomainUsernamePasswordAuthenticationToken into our authenticate method.
  2. We then use the domain information to create our email address and authenticate, as we had previously done. Admittedly, this example is contrived. However, the example is able to illustrate how to authenticate with an additional parameter.
  3. The CalendarUserAuthenticationProvider interface can now use the new domain field. However, there is no way for a user to specify the domain. For this, we must update our login.html file.

将域添加到登录页面

打开 login.html 文件并添加一个名为 domain 的新输入,如下所示:

    //src/main/resources/templates/login.html

    ...
    <label for="username">Username</label>
    <input type="text" id="username" name="username"/>
    <label for="password">Password</label>
    <input type="password" id="password" name="password"/>
    <label for="domain">Domain</label>
    <input type="text" id="domain" name="domain"/>

现在,当用户尝试登录时,将提交一个域。但是,Spring Security 不知道如何使用该域来创建 DomainUsernamePasswordAuthenticationToken 对象并将其传递给 AuthenticationProvider .为了解决这个问题,我们需要创建 DomainUsernamePasswordAuthenticationFilter

DomainUsernamePasswordAuthenticationFilter 类

Spring Security 提供了许多 servlet 过滤器,它们充当控制器来验证用户。过滤器作为我们在 FilterChainProxy 对象的委托之一调用="ch02lvl1sec07">第 2 章Spring Security 入门。以前,formLogin() 方法指示 Spring Security 使用 o.s.s.web.authentication.UsernamePasswordAuthenticationFilter 作为登录控制器。过滤器的工作是执行以下任务:

  • Obtain a username and password from the HTTP request.
  • Create a UsernamePasswordAuthenticationToken object with the information obtained from the HTTP request.
  • Request that Spring Security validates UsernamePasswordAuthenticationToken.
  • If the token is validated, it will set the authentication returned to it on SecurityContextHolder, just as we did when a new user signed up for an account. We will need to extend UsernamePasswordAuthenticationFilter to leverage our newly created DoainUsernamePasswordAuthenticationToken object.
  • Create a DomainUsernamePasswordAuthenticationFilter object, as follows:
        //src/main/java/com/packtpub/springsecurity/web/authentication/
        DomainUsernamePasswordAuthenticationFilter.java

        public final class
        DomainUsernamePasswordAuthenticationFilter extends 
         UsernamePasswordAuthenticationFilter {
        public Authentication attemptAuthentication
        (HttpServletRequest request,HttpServletResponse response) throws
        AuthenticationException {
               if (!request.getMethod().equals("POST")) {
                 throw new AuthenticationServiceException
                 ("Authentication method not supported: " 
                  + request.getMethod());
               }
           String username = obtainUsername(request);
           String password = obtainPassword(request);
           String domain = request.getParameter("domain");
           // authRequest.isAuthenticated() = false since no
           //authorities are specified
           DomainUsernamePasswordAuthenticationToken authRequest
           = new DomainUsernamePasswordAuthenticationToken(username, 
           password, domain);
          setDetails(request, authRequest);
          return this.getAuthenticationManager()
          .authenticate(authRequest);
          }
        }

新的 DomainUsernamePasswordAuthenticationFilter 对象将执行以下任务:

  • Obtain a username, password, and domain from the HttpServletRequest method.
  • Create our DomainUsernamePasswordAuthenticationToken object with information obtained from the HTTP request.
  • Request that Spring Security validates DomainUsernamePasswordAuthenticationToken. The work is delegated to CalendarUserAuthenticationProvider.
  • If the token is validated, its superclass will set the authentication returned by CalendarUserAuthenticationProvider on SecurityContextHolder, just as we did to authenticate a user after they created a new account.

更新我们的配置

现在我们已经创建了附加参数所需的所有代码,我们需要配置 Spring Security 以了解它。以下代码片段包括对我们的 SecurityConfig.java 文件的必要更新,以支持我们的附加参数:

//src/main/java/com/packtpub/springsecurity/configuration/
SecurityConfig.java

@Override
protected void configure(final HttpSecurity http) throws Exception {
   http.authorizeRequests()
       ...
       .and().exceptionHandling()
           .accessDeniedPage("/errors/403")
           .authenticationEntryPoint(
               loginUrlAuthenticationEntryPoint())
       .and().formLogin()
           .loginPage("/login/form")
           .loginProcessingUrl("/login")
           .failureUrl("/login/form?error")
           .usernameParameter("username")
           .passwordParameter("password")
           .defaultSuccessUrl("/default", true)
           .permitAll()
         ...
          // Add custom UsernamePasswordAuthenticationFilter
           .addFilterAt(
               domainUsernamePasswordAuthenticationFilter(),
              UsernamePasswordAuthenticationFilter.class)
   ;
}
@Bean
public DomainUsernamePasswordAuthenticationFilter domainUsernamePasswordAuthenticationFilter()
       throws Exception {
   DomainUsernamePasswordAuthenticationFilter dupaf = new DomainUsernamePasswordAuthenticationFilter(
                            super.authenticationManagerBean());
   dupaf.setFilterProcessesUrl("/login");
   dupaf.setUsernameParameter("username");
   dupaf.setPasswordParameter("password");
   dupaf.setAuthenticationSuccessHandler(
           new SavedRequestAwareAuthenticationSuccessHandler(){{
               setDefaultTargetUrl("/default");
           }}
   );
   dupaf.setAuthenticationFailureHandler(
           new SimpleUrlAuthenticationFailureHandler(){{
                setDefaultFailureUrl("/login/form?error");
           }}
);
 dupaf.afterPropertiesSet();
   return dupaf;
}
@Bean
public LoginUrlAuthenticationEntryPoint loginUrlAuthenticationEntryPoint(){
   return new LoginUrlAuthenticationEntryPoint("/login/form");
}
前面的代码片段在我们的 Spring Security 配置中配置了标准 bean。我们已经证明了这一点是可以做到的。然而,在本书的大部分内容中,我们将标准 bean 配置包含在其自己的文件中,因为这使得配置不那么冗长。如果您遇到问题,或者不想输入所有这些,您可以从 chapter03.06-日历

以下是配置更新的一些亮点:

  • We overrode defaultAuthenticationEntryPoint and added a reference to o.s.s.web.authentication.LoginUrlAuthenticationEntryPoint, which determines what happens when a request for a protected resource occurs and the user is not authenticated. In our case, we are redirected to a login page.
  • We removed the formLogin() method and used a .addFilterAt() method to insert our custom filter into FilterChainProxy. The position indicates the order in which the delegates of FilterChain are considered and cannot overlap with another filter, but can replace the filter at the current position. We replaced UsernamePasswordAuthenticationFilter with our custom filter.
  • We added the configuration for our custom filter, which refers to the authentication manager created by the configure(AuthenticationManagerBuilder) method.

请看下图供您参考:

读书笔记《spring-security-third-edition》自定义身份验证

您现在可以重新启动应用程序并尝试上图所示的以下步骤,以了解所有部分如何组合在一起:

  1. Visit http://localhost:8080/events.
  2. Spring Security will intercept the secured URL and use the LoginUrlAuthenticationEntryPoint object to process it.
  3. The LoginUrlAuthenticationEntryPoint object will send the user to the login page. Enter admin1 as the username, example.com as the domain, and admin1 as the password.
  4. The DomainUsernamePasswordAuthenticationFilter object will intercept the process of the login request. It will then obtain the username, domain, and password from the HTTP request and create a DomainUsernamePasswordAuthenticationToken object.
  5. The DomainUsernamePasswordAuthenticationFilter object submits DomainUsernamePasswordAuthenticationToken to CalendarUserAuthenticationProvider.
  6. The CalendarUserAuthenticationProvider interface validates DomainUsernamePasswordAuthenticationToken and then returns an authenticated DomainUsernamePasswordAuthenticationToken object (that is, isAuthenticated() returns true).
  7. The DomainUserPasswordAuthenticationFilter object updates SecurityContext with DomainUsernamePasswordAuthenticationToken and places it on SecurityContextHolder.
您的代码应该类似于 chapter03.06-calendar

使用哪种身份验证方法?

我们已经介绍了三种主要的身份验证方法,那么哪一种是最好的呢?像所有解决方案一样,每个解决方案都有其优点和缺点。您可以通过参考以下列表找到有关何时使用特定类型身份验证的摘要:

  • SecurityContextHolder: Interacting directly with SecurityContextHolder is certainly the easiest way of authenticating a user. It works well when you are authenticating a newly created user or authenticating in an unconventional way. By using SecurityContextHolder directly, we do not have to interact with so many Spring Security layers. The downside is that we do not get some of the more advanced features that Spring Security provides automatically. For example, if we want to send the user to the previously requested page after logging in, we would have to manually integrate that into our controller.
  • UserDetailsService: Creating a custom UserDetailsService object is an easy mechanism that allows for Spring Security to make security decisions based on our custom domain model. It also provides a mechanism to hook into other Spring Security features. For example, Spring Security requires UserDetailsService in order to use the built-in remember-me support covered in Chapter 7, Remember-Me Services. The UserDetailsService object does not work when authentication is not based on a username and password.
  • AuthenticationProvider: This is the most flexible method for extending Spring Security. It allows a user to authenticate with any parameters that we wish. However, if we wish to leverage features such as Spring Security's remember-me, we will still need UserDetailsService.

概括

本章使用现实世界的问题来介绍 Spring Security 中使用的基本构建块。它还向我们展示了如何通过扩展这些基本构建块来使 Spring Security 针对我们的自定义域对象进行身份验证。总之,我们了解到SecurityContextHolder接口是确定当前用户的中心位置。开发者不仅可以使用它来访问当前用户,还可以设置当前登录的用户。

我们还探讨了如何创建自定义 UserDetailsS​​erviceAuthenticationProvider 对象,以及如何使用不仅仅是用户名和密码来执行身份验证。

在下一章中,我们将探讨一些对基于 JDBC 的身份验证的内置支持。