Spring Security 额外登录字段

1. 简介

在本文中,我们将通过向标准登录表单添加一个额外字段来使用Spring Security实现自定义身份验证场景。

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

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

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

我们将建立在我们之前关于 Spring Security login的文章中讨论的概念之上。

2. Maven 设置

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

我们将使用的设置需要父声明、Web 启动器和安全启动器;我们还将包括 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>

可以[在 Maven Central](https://search.maven.org/classic/#search|ga|1|g%3A"org.springframework.boot" AND a%3A"spring-boot-starter-security")找到最新版本的 Spring Boot security starter 。

3. 简单的项目设置

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

关键组成部分将包括:

  • SimpleAuthenticationFilter – UsernamePasswordAuthenticationFilter的扩展
  • SimpleUserDetailsService – UserDetailsS ervice的一个实现
  • User – Spring Security提供的User类的扩展,它声明了额外的域字段
  • SecurityConfig – Spring Security配置,它将SimpleAuthenticationFilter插入过滤器链,声明安全规则并连接依赖项
  • login.html – 一个登录页面,包含用户名、密码和域

3.1. 简单的身份验证过滤器

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

然后将令牌传递给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. 简单 UserDetails 服务

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

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 配置

我们的设置与标准的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");
}

我们能够使用提供的DaoAuthenticationProvider,因为我们用SimpleUserDetailsService配置了它。回想一下,我们的SimpleUserDetailsService知道如何解析用户名和域字段,并在身份验证时返回适当的用户:

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

因为我们使用了SimpleAuthenticationFilter,所以我们配置了自己的AuthenticationFailureHandler,以确保正确地处理失败的登录尝试:

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

3.4. 登录页面

我们使用的登录页面收集由我们的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>

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

3.5. 总结

在我们的第一个示例中,我们能够通过“伪造”用户名字段来重用DaoAuthenticationProvider和UsernamePasswordAuthenticationToken。

因此,我们能够通过最少的配置和额外的代码来添加对额外登录字段的支持

4. 自定义项目设置

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

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

  • CustomAuthenticationFilter: UsernamePasswordAuthenticationFilter的扩展
  • CustomUserDetailsService: 自定义接口,声明loadUserbyUsernameAndDomain方法
  • CustomUserDetailsServiceImpl: CustomUserDetailsService的实现
  • CustomUserDetailsAuthenticationProvider: 对AbstractUserDetailsAuthenticationProvider的扩展
  • CustomAuthenticationToken: UsernamePasswordAuthenticationToken的扩展
  • User: Spring Security提供的User类的扩展,它声明了额外的域字段
  • SecurityConfig: Spring Security配置,将CustomAuthenticationFilter插入到过滤器链中,声明安全规则并连接依赖
  • login.html: 包含用户名、密码和域的登录页面

4.1. 自定义身份验证过滤器

在我们的CustomAuthenticationFilter中,我们从请求中提取用户名、密码和域字段。这些值用于创建我们的自定义AuthenticationToken实例,该实例将传递给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. 自定义 UserDetails 服务

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

我们创建的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. 自定义 UserDetailsAuthenticationProvider

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

请注意,我们必须将身份验证令牌转换为我们的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. 总结

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

5. 结论

在本文中,我们在 Spring Security 中实现了一个使用额外登录字段的表单登录。我们以两种不同的方式做到了这一点:

  • 在我们的简单方法中,我们最大限度地减少了需要编写的代码量。通过使用自定义解析逻辑调整用户名,我们能够重用DaoAuthenticationProviderUsernamePasswordAuthentication
  • 在我们更加自定义的方法中,我们通过扩展 AbstractUserDetailsAuthenticationProvider 并提供我们自己的 CustomUserDetailsServiceCustomAuthenticationToken来提供自定义字段支持.

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Spring Security可以通过自定义登录页面来实现用户认证。具体步骤如下: 1. 创建一个登录页面,可以使用JSP、Thymeleaf等模板引擎来实现。 2. 在Spring Security配置文件中,配置登录页面的URL和处理登录请求的URL。 3. 在登录页面中,使用表单提交用户输入的用户名和密码。 4. 在Spring Security配置文件中,配置用户认证的方式,可以使用内存认证、数据库认证、LDAP认证等方式。 5. 在用户认证成功后,可以使用Spring Security提供的默认跳转页面,也可以自定义跳转页面。 6. 在用户认证失败后,可以在登录页面中显示错误信息,或者跳转到自定义的错误页面。 以上就是Spring Security自定义登录页面的基本步骤。 ### 回答2: Spring Security是一个用于认证和授权的框架,可以帮助我们实现安全的用户登录和权限管理。在Spring Security中,我们可以自定义登录页面来满足自己的需求。 首先,我们需要创建一个登录页面的JSP文件,可以放在项目的WEB-INF目录下。在该页面中,我们可以设计自己需要的输入框,例如用户名和密码的输入框,以及登录按钮。 接下来,在Spring Security的配置文件中,我们需要指定我们自定义的登录页面。可以使用`http.formLogin().loginPage("/login")`来指定登录页面的URL路径。 然后,我们需要编写一个用于处理登录请求的Controller。该Controller需要处理用户输入的用户名和密码,并进行相应的验证。验证通过后,可以使用Spring Security提供的API来进行登录操作。 在登录验证成功后,我们可以根据需要进行跳转,例如跳转到用户的首页或者其他页面。 另外,我们还可以对登录页面进行一些额外的自定义操作,例如添加验证码功能、记住我功能等等。通过自定义登录页面,我们可以根据自己的需求对用户登录进行个性化设计,提供更好的用户体验。 总结起来,自定义登录页面就是通过创建一个自定义的JSP文件,在Spring Security的配置中指定该JSP文件的URL路径,然后编写一个处理登录请求的Controller来验证用户输入的用户名和密码,并进行相应的登录操作。自定义登录页面可以让我们更好地满足项目的需求,提高用户的登录体验。 ### 回答3: Spring Security是一个功能强大的安全框架,可以用于保护和管理Web应用程序的身份验证和授权。默认情况下,Spring Security提供了一个简单的登录页面,但我们可以根据我们的需求自定义登录页面。 首先,我们需要创建一个自定义的登录页面。我们可以使用HTML、CSS和JavaScript来设计并构建登录页面。登录页面可以包含用户名和密码输入字段,以及登录按钮。可以添加额外的功能,如“忘记密码”链接或“注册”按钮,根据需求进行自定义。 在Spring Security的配置文件中,我们需要指定自定义登录页面的URL,并对其进行保护。我们可以使用Java配置或XML配置来完成这个步骤。例如,在Java配置中,我们可以使用`http.formLogin().loginPage("/custom-login")`来指定自定义登录页面的URL。 接下来,我们需要在自定义登录页面的表单中添加与Spring Security相关的字段。这些字段通常是“username”和“password”,以便Spring Security可以正确地验证用户凭据。 在后端,我们需要编写一个处理用户认证的逻辑。我们可以实现`UserDetailsService`接口来加载和验证用户的凭据。我们也可以自定义`AuthenticationProvider`来处理用户的认证请求,并提供自定义的逻辑。 最后,我们需要将我们的自定义登录页面与Spring Security整合起来。我们可以在Spring Security的配置文件中添加一个身份验证过滤器,将自定义登录页面的URL与处理用户认证的逻辑连接起来。 总结起来,要自定义Spring Security登录页面,我们需要创建一个自定义的登录页面,指定其URL,并对其进行保护。然后,我们需要在表单中添加与Spring Security相关的字段,并编写处理用户认证的逻辑。最后,将自定义登录页面与Spring Security整合起来。这样,我们就可以按照我们的需求来设计和实现自定义的登录页面。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值