Extra Login Fields with Spring Security – 使用Spring Security的额外登录字段

最后修改: 2018年 1月 25日

中文/混合/英文(键盘快捷键:t)

1. Introduction

1.介绍

In this article, we’ll implement a custom authentication scenario with Spring Security by adding an extra field to the standard login form.

在本文中,我们将通过Spring Security实现自定义身份验证方案,在标准登录表单中添加一个额外字段

We’re going to focus on 2 different approaches, to show the versatility of the framework and the flexible ways we can use it in.

我们将专注于2种不同的方法,以展示该框架的多功能性和我们可以用它的灵活方式。

Our first approach will be a simple solution which focuses on reuse of existing core Spring Security implementations.

我们的第一个方法将是一个简单的解决方案,其重点是重用现有的核心Spring Security实现。

Our second approach will be a more custom solution that may be more suitable for advanced use cases.

我们的第二种方法将是一个更加定制的解决方案,可能更适合高级用例。

We’ll build on top of concepts that are discussed in our previous articles on Spring Security login.

我们将在以前关于Spring安全登录的文章中所讨论的概念基础上进行探讨

2. Maven Setup

2.Maven的设置

We’ll use Spring Boot starters to bootstrap our project and bring in all necessary dependencies.

我们将使用Spring Boot启动器来启动我们的项目并引入所有必要的依赖。

The setup we’ll use requires a parent declaration, web starter, and security starter; we’ll also include thymeleaf :

我们将使用的设置需要一个父声明、网络启动器和安全启动器;我们还将包括百里香。

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.6.1</version>
    <relativePath/>
</parent>
 
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
     </dependency>
     <dependency>
        <groupId>org.thymeleaf.extras</groupId>
        <artifactId>thymeleaf-extras-springsecurity5</artifactId>
    </dependency>
</dependencies>

The most current version of Spring Boot security starter can be found over at Maven Central.

最新版本的Spring Boot安全启动器可以在Maven Central上找到

3. Simple Project Setup

3.简单的项目设置

In our first approach, we’ll focus on reusing implementations that are provided by Spring Security. In particular, we’ll reuse DaoAuthenticationProvider and UsernamePasswordToken as they exist “out-of-the-box”.

在我们的第一个方法中,我们将专注于重用Spring Security所提供的实现。特别是,我们将重用DaoAuthenticationProviderUsernamePasswordToken,因为它们已经 “开箱即用”。

The key components will include:

关键部分将包括。

  • SimpleAuthenticationFilteran extension of UsernamePasswordAuthenticationFilter
  • SimpleUserDetailsServicean implementation of UserDetailsService
  • Useran extension of the User class provided by Spring Security that declares our extra domain field
  • SecurityConfigour Spring Security configuration that inserts our SimpleAuthenticationFilter into the filter chain, declares security rules and wires up dependencies
  • login.html a login page that collects the username, password, and domain

3.1. Simple Authentication Filter

3.1.简单认证过滤器

In our SimpleAuthenticationFilter, the domain and username fields are extracted from the request. We concatenate these values and use them to create an instance of UsernamePasswordAuthenticationToken.

在我们的SimpleAuthenticationFilter中,从请求中提取了域名和用户名字段。我们将这些值连接起来,并使用它们来创建一个UsernamePasswordAuthenticationToken的实例。

The token is then passed along to the AuthenticationProvider for authentication:

然后将令牌传递给AuthenticationProvider进行认证

public class SimpleAuthenticationFilter
  extends UsernamePasswordAuthenticationFilter {

    @Override
    public Authentication attemptAuthentication(
      HttpServletRequest request, 
      HttpServletResponse response) 
        throws AuthenticationException {

        // ...

        UsernamePasswordAuthenticationToken authRequest
          = getAuthRequest(request);
        setDetails(request, authRequest);
        
        return this.getAuthenticationManager()
          .authenticate(authRequest);
    }

    private UsernamePasswordAuthenticationToken getAuthRequest(
      HttpServletRequest request) {
 
        String username = obtainUsername(request);
        String password = obtainPassword(request);
        String domain = obtainDomain(request);

        // ...

        String usernameDomain = String.format("%s%s%s", username.trim(), 
          String.valueOf(Character.LINE_SEPARATOR), domain);
        return new UsernamePasswordAuthenticationToken(
          usernameDomain, password);
    }

    // other methods
}

3.2. Simple UserDetails Service

3.2.简单的UserDetails服务

The UserDetailsService contract defines a single method called loadUserByUsername. Our implementation extracts the username and domain. The values are then passed to our UserRepository to get the User:

UserDetailsService合约定义了一个名为loadUserByUsername的单一方法。我们的实现提取了用户名域名。然后将这些值传递给我们的UserRepository以获得User

public class SimpleUserDetailsService implements UserDetailsService {

    // ...

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        String[] usernameAndDomain = StringUtils.split(
          username, String.valueOf(Character.LINE_SEPARATOR));
        if (usernameAndDomain == null || usernameAndDomain.length != 2) {
            throw new UsernameNotFoundException("Username and domain must be provided");
        }
        User user = userRepository.findUser(usernameAndDomain[0], usernameAndDomain[1]);
        if (user == null) {
            throw new UsernameNotFoundException(
              String.format("Username not found for domain, username=%s, domain=%s", 
                usernameAndDomain[0], usernameAndDomain[1]));
        }
        return user;
    }
}

3.3. Spring Security Configuration

3.3.Spring安全配置

Our setup is different from a standard Spring Security configuration because we insert our SimpleAuthenticationFilter into the filter chain before the default with a call to addFilterBefore:

我们的设置与标准的Spring Security配置不同,因为我们通过调用addFilterBefore将我们的SimpleAuthenticationFilter插入到默认的过滤器链中

@Override
protected void configure(HttpSecurity http) throws Exception {

    http
      .addFilterBefore(authenticationFilter(), 
        UsernamePasswordAuthenticationFilter.class)
      .authorizeRequests()
        .antMatchers("/css/**", "/index").permitAll()
        .antMatchers("/user/**").authenticated()
      .and()
      .formLogin().loginPage("/login")
      .and()
      .logout()
      .logoutUrl("/logout");
}

We’re able to use the provided DaoAuthenticationProvider because we configure it with our SimpleUserDetailsService. Recall that our SimpleUserDetailsService knows how to parse out our username and domain fields and return the appropriate User to use when authenticating:

我们能够使用所提供的DaoAuthenticationProvider,因为我们在SimpleUserDetailsService中配置了它。回顾一下,我们的SimpleUserDetailsService知道如何解析出我们的usernamedomain字段并返回适当的User,以便在认证时使用。

public AuthenticationProvider authProvider() {
    DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
    provider.setUserDetailsService(userDetailsService);
    provider.setPasswordEncoder(passwordEncoder());
    return provider;
}

Since we’re using a SimpleAuthenticationFilter, we configure our own AuthenticationFailureHandler to ensure failed login attempts are appropriately handled:

由于我们使用的是SimpleAuthenticationFilter,我们配置自己的AuthenticationFailureHandler,以确保登录失败的尝试得到适当处理。

public SimpleAuthenticationFilter authenticationFilter() throws Exception {
    SimpleAuthenticationFilter filter = new SimpleAuthenticationFilter();
    filter.setAuthenticationManager(authenticationManagerBean());
    filter.setAuthenticationFailureHandler(failureHandler());
    return filter;
}

3.4. Login Page

3.4.登录页面

The login page we use collects our additional domain field that gets extracted by our SimpleAuthenticationFilter:

我们使用的登录页面收集了额外的domain字段,该字段被我们的SimpleAuthenticationFilter提取出来。

<form class="form-signin" th:action="@{/login}" method="post">
 <h2 class="form-signin-heading">Please sign in</h2>
 <p>Example: user / domain / password</p>
 <p th:if="${param.error}" class="error">Invalid user, password, or domain</p>
 <p>
   <label for="username" class="sr-only">Username</label>
   <input type="text" id="username" name="username" class="form-control" 
     placeholder="Username" required autofocus/>
 </p>
 <p>
   <label for="domain" class="sr-only">Domain</label>
   <input type="text" id="domain" name="domain" class="form-control" 
     placeholder="Domain" required autofocus/>
 </p>
 <p>
   <label for="password" class="sr-only">Password</label>
   <input type="password" id="password" name="password" class="form-control" 
     placeholder="Password" required autofocus/>
 </p>
 <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button><br/>
 <p><a href="/index" th:href="@{/index}">Back to home page</a></p>
</form>

When we run the application and access the context at http://localhost:8081, we see a link to access a secured page. Clicking the link will cause the login page to display. As expected, we see the additional domain field:

当我们运行应用程序并在http://localhost:8081访问上下文时,我们看到一个访问安全页面的链接。点击该链接将导致显示登录页面。正如预期的那样,我们看到了额外的域字段

Spring Security Extra Fields Login page

3.5. Summary

3.5.摘要

In our first example, we were able to reuse DaoAuthenticationProvider and UsernamePasswordAuthenticationToken by “faking out” the username field.

在我们的第一个例子中,我们能够通过 “伪造 “用户名字段来重复使用DaoAuthenticationProviderUsernamePasswordAuthenticationToken

As a result, we were able to add support for an extra login field with a minimal amount of configuration and additional code.

因此,我们能够以最少的配置和额外的代码增加对一个额外的登录字段的支持

4. Custom Project Setup

4.自定义项目设置

Our second approach will be very similar to the first but may be more appropriate for non-trivial uses cases.

我们的第二种方法将与第一种方法非常相似,但可能更适合于非琐碎的用例。

The key components of our second approach will include:

我们第二个方法的关键组成部分将包括。

  • CustomAuthenticationFilteran extension of UsernamePasswordAuthenticationFilter
  • CustomUserDetailsServicea custom interface declaring a loadUserbyUsernameAndDomain method
  • CustomUserDetailsServiceImplan implementation of our CustomUserDetailsService
  • CustomUserDetailsAuthenticationProvideran extension of AbstractUserDetailsAuthenticationProvider
  • CustomAuthenticationTokenan extension of UsernamePasswordAuthenticationToken
  • Useran extension of the User class provided by Spring Security that declares our extra domain field
  • SecurityConfigour Spring Security configuration that inserts our CustomAuthenticationFilter into the filter chain, declares security rules and wires up dependencies
  • login.html the login page that collects the username, password, and domain

4.1. Custom Authentication Filter

4.1.自定义认证过滤器

In our CustomAuthenticationFilter, we extract the username, password, and domain fields from the request. These values are used to create an instance of our CustomAuthenticationToken which is passed to the AuthenticationProvider for authentication:

在我们的CustomAuthenticationFilter中,我们从请求中提取了用户名、密码和域字段。这些值被用来创建我们的CustomAuthenticationToken实例,该实例被传递给AuthenticationProvider以进行认证。

public class CustomAuthenticationFilter 
  extends UsernamePasswordAuthenticationFilter {

    public static final String SPRING_SECURITY_FORM_DOMAIN_KEY = "domain";

    @Override
    public Authentication attemptAuthentication(
        HttpServletRequest request,
        HttpServletResponse response) 
          throws AuthenticationException {

        // ...

        CustomAuthenticationToken authRequest = getAuthRequest(request);
        setDetails(request, authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);
    }

    private CustomAuthenticationToken getAuthRequest(HttpServletRequest request) {
        String username = obtainUsername(request);
        String password = obtainPassword(request);
        String domain = obtainDomain(request);

        // ...

        return new CustomAuthenticationToken(username, password, domain);
    }

4.2. Custom UserDetails Service

4.2.自定义UserDetails服务

Our CustomUserDetailsService contract defines a single method called loadUserByUsernameAndDomain.

我们的CustomUserDetailsService合约定义了一个名为loadUserByUsernameAndDomain的单一方法。

The CustomUserDetailsServiceImpl class we create simply implements the contract and delegates to our CustomUserRepository to get the User:

我们创建的CustomUserDetailsServiceImpl类简单地实现了合同,并委托给我们的CustomUserRepository来获取User

 public UserDetails loadUserByUsernameAndDomain(String username, String domain) 
     throws UsernameNotFoundException {
     if (StringUtils.isAnyBlank(username, domain)) {
         throw new UsernameNotFoundException("Username and domain must be provided");
     }
     User user = userRepository.findUser(username, domain);
     if (user == null) {
         throw new UsernameNotFoundException(
           String.format("Username not found for domain, username=%s, domain=%s", 
             username, domain));
     }
     return user;
 }

4.3. Custom UserDetailsAuthenticationProvider

4.3.自定义UserDetailsAuthenticationProvider

Our CustomUserDetailsAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider and delegates to our CustomUserDetailService to retrieve the User. The most important feature of this class is the implementation of the retrieveUser method.

我们的CustomUserDetailsAuthenticationProvider扩展了AbstractUserDetailsAuthenticationProvider并委托给我们的CustomUserDetailService来检索User这个类最重要的特征是实现retrieveUser方法

Note that we must cast the authentication token to our CustomAuthenticationToken for access to our custom field:

请注意,我们必须将认证令牌投给我们的CustomAuthenticationToken,以便访问我们的自定义字段。

@Override
protected UserDetails retrieveUser(String username, 
  UsernamePasswordAuthenticationToken authentication) 
    throws AuthenticationException {
 
    CustomAuthenticationToken auth = (CustomAuthenticationToken) authentication;
    UserDetails loadedUser;

    try {
        loadedUser = this.userDetailsService
          .loadUserByUsernameAndDomain(auth.getPrincipal()
            .toString(), auth.getDomain());
    } catch (UsernameNotFoundException notFound) {
 
        if (authentication.getCredentials() != null) {
            String presentedPassword = authentication.getCredentials()
              .toString();
            passwordEncoder.matches(presentedPassword, userNotFoundEncodedPassword);
        }
        throw notFound;
    } catch (Exception repositoryProblem) {
 
        throw new InternalAuthenticationServiceException(
          repositoryProblem.getMessage(), repositoryProblem);
    }

    // ...

    return loadedUser;
}

4.4. Summary

4.4.总结

Our second approach is nearly identical to the simple approach we presented first. By implementing our own AuthenticationProvider and CustomAuthenticationToken, we avoided needing to adapt our username field with custom parsing logic.

我们的第二个方法与我们首先介绍的简单方法几乎相同。通过实现我们自己的AuthenticationProviderCustomAuthenticationToken,我们避免了用自定义解析逻辑来调整我们的用户名字段。

5. Conclusion

5.结论

In this article, we’ve implemented a form login in Spring Security that made use of an extra login field. We did this in 2 different ways:

在这篇文章中,我们在Spring Security中实现了一个表单登录,它利用了一个额外的登录字段。我们通过2种不同的方式实现了这一点。

  • In our simple approach, we minimized the amount of code we needed write. We were able to reuse DaoAuthenticationProvider and UsernamePasswordAuthentication by adapting the username with custom parsing logic
  • In our more customized approach, we provided custom field support by extending AbstractUserDetailsAuthenticationProvider and providing our own CustomUserDetailsService with a CustomAuthenticationToken

As always, all source code can be found over on GitHub.

一如既往,所有源代码都可以在GitHub上找到