如何使用Spring Boot reCapture

404 阅读8分钟

开始使用Spring Boot reCapture

reCAPTCHA是一个系统,使用户能够保护他们的网站免受机器人攻击。ReCaptcha生成一个分数,范围从01 。如果分数小于0.5 ,就有可能是机器人调用的动作。

本教程将教你如何实施Google reCAPTCHA第3版来保护Spring Boot登录表单。我们将在登录过程中拦截认证,并检查生成的分数是否低于0.5 ,然后要求用户输入发送到其电子邮件的OTP。否则,我们就对用户进行认证。

前提条件

  • 有[Spring Boot]方面的知识。
  • [Thymeleaf]方面的知识。
  • 在你的电脑上安装[JDK 11+]。
  • 在你的电脑上安装[Intellij IDEA]。

创建Google recapture项目

[创建一个新的reCAPTCHA V3网站] ,名字可以是你想要的。

将域名设置为localhost,因为我们将在本地测试该应用程序。注意网站的密钥和密匙,我们将在以后的应用程序中使用它们。

创建一个Spring Boot应用程序

导航到Spring Initialzr,生成一个新的项目,其依赖项为:Spring Web,Spring Security,Spring Data JPA,Thymeleaf,Spring Boot Dev Tools,MySQL DriverLombok

将生成的项目导入Intellij,并确保你有一个活跃的互联网连接,以便从远程中央资源库下载依赖项。

Spring Initializr image

数据库配置

由于我们不会将应用程序部署到生产中,数据库表将在应用程序初始化时自动创建。这是因为我们设置了spring.jpa.hibernate.ddl-auto=create 属性。

将下面的属性添加到application.properties 文件中。

spring.datasource.url=jdbc:mysql://localhost:3306/recaptcha
spring.datasource.username=username
spring.datasource.password=password
spring.jpa.hibernate.ddl-auto=create
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect

域层

我们将使用项目Lombok ,为项目中的所有模型生成getters和setters,一个无参数的构造函数,以及一个全参数的构造函数。

创建一个带有字段idrole nameRole 类,其中角色名称代表一个用户的权限。

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

import javax.persistence.*;

@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Entity(name = "role")
@Table
public class Role {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String roleName;

    public Role(String roleName) {
        this.roleName = roleName;
    }
}
  • @Data 为该类中的字段生成所有的getters和setters。
  • @AllArgsConstructor 生成一个带有该类中声明的所有字段的构造函数。
  • @NoArgsConstructor 生成一个没有任何参数的构造函数。
  • @ToString 生成一个字段的字符串表示,我们可以在调试时使用。

创建一个名为AppUser 的类,其字段id,username, 和password 代表不同的用户,他们可以根据自己的权限与应用程序进行交互。

由于用户可以有很多权限,我们必须在AppUser 类中包含一个角色字段,并使用@ManyToMany 注释对其进行标注。

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.Collection;

@Entity(name = "user")
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class AppUser {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(name = "user_name")
    private String userName;

    @Column(name = "password")
    private String password;

    @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
    private Collection<Role> roles = new ArrayList<>();

    public AppUser(String userName, String password) {
        this.userName = userName;
        this.password = password;
    }
}

创建一个名为ReCaptchaResponse 的类,其字段为success,hostname,action,score,challenge_ts, 和errorCodes ,这是一个字符串数组。用@JsonProperty("error-codes") 注释它,表明来自服务器的响应是一个JSON ,并映射到一个字符串数组。

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;

@Data
public class ReCaptchaResponse {
    private boolean success;
    private String hostname;
    private String action;
    private float score;
    private String challenge_ts;

    @JsonProperty("error-codes")
    private String[] errorCodes;
}

存储库层

创建一个名为AppUserRepository 的接口,扩展到JpaRepository 。这个接口将允许我们在不写任何查询的情况下进行CRUD操作。

创建一个方法,当用户名被传递给它时返回一个AppUser 。我们可以通过添加以下方法来实现这个目的。

import org.springframework.data.jpa.repository.JpaRepository;

public interface AppUserRepository extends JpaRepository<AppUser, Long> {
    AppUser findAppUserByUserName(String username);
}

像我们之前做的那样创建一个名为RoleRepository 的接口,并添加一个方法,通过搜索角色名称返回一个角色。

import org.springframework.data.jpa.repository.JpaRepository;


public interface RoleRepository extends JpaRepository<Role, Long> {
    Role findRoleByRoleName(String roleName);
}

配置层

在应用类中,创建一个密码编码器Bean ,我们将用它来对密码进行编码,然后再保存到数据库中。

@Bean
PasswordEncoder passwordEncoder(){
    return new BCryptPasswordEncoder();
}

在应用程序类中,创建一个CommandLineRunner Bean,我们将用它来创建自定义用户并在应用程序启动时测试我们的应用程序。

该命令行运行器将接受三个参数。AppUserRepository,RoleRepository, 和PasswordEncoder

@Bean
CommandLineRunner commandLineRunner(AppUserRepository appUserRepository,
    RoleRepository roleRepository,
    PasswordEncoder passwordEncoder) {
    return args - > {

    }
}

在命令的lambda函数里面,命令行运行器创建一个角色列表,并使用RoleRepository ,用saveAll 方法将角色保存在数据库中。

return args -> {
            List<Role> roles = List.of(
                    new Role("ROLE_USER"),
                    new Role("ROLE_ADMIN")
            );

            roleRepository.saveAll(roles);
}

在箭头函数中创建两个用户,用户名johndoemarypublic ,两个用户的密码1234

return args - > {
    List < Role > roles = List.of(
        new Role("ROLE_USER"),
        new Role("ROLE_ADMIN")
    );

    roleRepository.saveAll(roles);


    List < AppUser > appUsers = List.of(
        new AppUser("johndoe", passwordEncoder.encode("1234")),
        new AppUser("marypublic", passwordEncoder.encode("1234"))
    );
    appUserRepository.saveAll(appUsers);
}

将角色ROLE_USER 添加到johndoe ,将角色ROLE_ADMIN 添加到marypublic ,并确保使用save() 方法将更改提交到数据库中。

应用程序类最终将如下图所示。

import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import java.util.List;

@SpringBootApplication
public class ReCaptchaApplication {

    public static void main(String[] args) {
        SpringApplication.run(ReCaptchaApplication.class, args);
    }

    @Bean
    PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    CommandLineRunner commandLineRunner(AppUserRepository appUserRepository,
                                        RoleRepository roleRepository,
                                        PasswordEncoder passwordEncoder){
        return args -> {
            List<Role> roles = List.of(
                    new Role("ROLE_USER"),
                    new Role("ROLE_ADMIN")
            );

            roleRepository.saveAll(roles);


            List<AppUser> appUsers = List.of(
                    new AppUser("johndoe",passwordEncoder.encode("1234")),
                    new AppUser("marypublic",passwordEncoder.encode("1234"))
            );
            appUserRepository.saveAll(appUsers);

            AppUser john = appUserRepository.findAppUserByUserName("johndoe");
            Role roleForJohn = roleRepository.findRoleByRoleName("ROLE_USER");
            john.getRoles().add(roleForJohn);
            appUserRepository.save(john);


            AppUser mary = appUserRepository.findAppUserByUserName("marypublic");
            Role roleForMary = roleRepository.findRoleByRoleName("ROLE_ADMIN");
            mary.getRoles().add(roleForMary);
            appUserRepository.save(mary);
        };
    }
}

服务层

创建一个名为AppUserService 的接口,扩展到UserDetailsService 。用户详情服务通过返回一个特定用户的用户名、密码和角色来验证用户。

import org.springframework.security.core.userdetails.UserDetailsService;

public interface AppUserService extends UserDetailsService {
}

创建一个名为AppUserServiceImpl 的类并实现AppUserService ,然后覆盖loadUserByUsername 方法来定位用户。

import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import javax.transaction.Transactional;
import java.util.ArrayList;
import java.util.Collection;

@Service
@RequiredArgsConstructor
@Transactional
public class  AppUserServiceImpl implements AppUserService{
    private final AppUserRepository appUserRepository;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        AppUser appUser = appUserRepository.findAppUserByUserName(username);
        if (username == null){
            throw new UsernameNotFoundException("user not registered");
        }else {
            Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
            appUser.getRoles().forEach(role -> {
                authorities.add(new SimpleGrantedAuthority(role.getRoleName()));
            });
            return new User(appUser.getUserName(),appUser.getPassword(),authorities);
        }

    }
}

创建一个名为AppConfig 的类,该类扩展了WebSecurityConfigurerAdapter 类,以向我们的应用程序添加自定义的安全信息。覆盖configureconfigure 方法。

在该类中为AppUserService 接口声明一个最终属性。@RequiredArgsConstructor 注解将创建一个构造函数,将字段声明为final。

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@Slf4j
public class AppConfig extends WebSecurityConfigurerAdapter {
    private final AppUserService appUserService;

     @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    }

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

}

在应用程序配置类中添加一个密码编码器Bean,数据访问对象认证提供者将使用它。

@Bean
BCryptPasswordEncoder bCryptPasswordEncoder(){
    return new BCryptPasswordEncoder();
}

创建一个方法DaoAuthenticationProvider ,并设置用户详细信息和密码编码器。

 @Bean
DaoAuthenticationProvider daoAuthenticationProvider(){
    DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
    daoAuthenticationProvider.setUserDetailsService(appUserService);
    daoAuthenticationProvider.setPasswordEncoder(bCryptPasswordEncoder());
    return daoAuthenticationProvider;
}

将DAO认证提供者添加到认证管理器构建器中。

 @Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.authenticationProvider(daoAuthenticationProvider());
}

configure 方法中配置将返回登录页面进程并在成功登录后重定向用户的路径。

/login 是返回登录表格的路径,/processLogin 是处理登录表格的路径,/success 是认证成功后返回成功页面的路径。

 @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http.cors().disable();
        http.
                .authorizeRequests()
                .anyRequest()
                .authenticated()
                .and()
                .formLogin()
                .loginPage("/login")
                .loginProcessingUrl("/processLogin")
                .defaultSuccessUrl("/success",true)
                .permitAll();
    }

安全和重新抓取设置

ReCaptcharV3Handler ,将用于验证表单的g-recaptcha-respone ,提供额外的信息,如秘密密钥和服务器的URL,以验证我们的响应。

验证成功后,这个类将返回一个JSON响应,它将映射到一个普通的java对象,以产生认证过滤器使用的ReCaptchaResponse 对象和分数。

在该类中,创建一个verify 方法,其参数为g-recatcha-response ,类型为字符串,将返回我们的分数的浮动值。

我们将记录所有从服务器返回的值,其中包括success,action,hostname,score,challenge, 和errorCodes ,以验证我们的reCAPTCHA 是否有效。

import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

public class ReCaptcharV3Handler {
    private String secretKey = "6Lc4JNUbAAAAANKqiE4UXytmsQw35UcHkzAScS_o";
    private String serverAddress = "https://www.google.com/recaptcha/api/siteverify";

    public float verify(String recaptchaFormResponse) throws InvalidReCaptchaTokenException{
        System.out.println("ReCaptcha v3 called.......");
        System.out.println("g-recaptcha-response: "+recaptchaFormResponse);

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

        MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
        map.add("secret",secretKey);
        map.add("response",recaptchaFormResponse);

        HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(map,headers);

        RestTemplate restTemplate = new RestTemplate();
        ReCaptchaResponse response = restTemplate.postForObject(
                serverAddress,request, ReCaptchaResponse.class);


        System.out.println("ReCaptcha response: \n");

        System.out.println("Success: "+response.isSuccess());
        System.out.println("Action: "+response.getAction());
        System.out.println("Hostname: "+response.getHostname());
        System.out.println("Score: "+response.getScore());
        System.out.println("Challenge Timestamp: "+response.getChallenge_ts());

        if (response.getErrorCodes() != null){
            System.out.println("Error codes: ");
            for (String errorCode: response.getErrorCodes()){
                System.out.println("\t" + errorCode);
            }
        }

        if (!response.isSuccess()){
            throw new InvalidReCaptchaTokenException("Invalid ReCaptha. Please check site");
        }
        // return 0.4f;
        return response.getScore();
    }
}

这个类将在认证发生之前拦截请求。

为了使拦截请求发挥作用,我们需要扩展spring security中的UsernamePasswordAuthenticationFilter 类。然后我们重写attemptAuthentication 方法,该方法接受HttpServeletRequestHttpServletResponse 参数。

通过请求,我们可以从URL中获取g-captcha-response ,并根据返回的分数,将其作为参数传递给验证方法。然后,我们可以允许认证继续进行,或者要求用户输入通过电子邮件发给他们的OTP。

创建一个名为CustomLoginFilter 的类,参数为loginURLhttpMethod ,类型为String。在构造函数中,调用父方法setRequiresAuthenticationRequestMatcher ,该方法接收loginURLhttpMethod 作为参数。

该方法将匹配给定路径和HTTP方法的请求,如果为真,我们将拦截该请求。

public CustomLoginFilter(String loginURL, String httpMethod){
    super.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher(loginURL,httpMethod));
}

CustomLoginFilter 类最终将如下图所示。

import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Slf4j
public class CustomLoginFilter extends UsernamePasswordAuthenticationFilter {

    public CustomLoginFilter(String loginURL, String httpMethod){
        super.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher(loginURL,httpMethod));
    }


    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        String recaptchaFormResponse = request.getParameter("g-recaptcha-response");

        System.out.println("Before processing authentication.......");

        ReCaptcharV3Handler handler = new ReCaptcharV3Handler();


        try {
           float score = handler.verify(recaptchaFormResponse);
            if (score < 0.5){
                    request.getRequestDispatcher("otp_login").forward(request,response);
            }
        } catch (InvalidReCaptchaTokenException | ServletException | IOException e) {
            try {
                response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
            } catch (IOException ioException) {
                ioException.printStackTrace();
            }
        }


        return super.attemptAuthentication(request, response);
    }
}

创建一个名为getCustomLoginFilter 的方法,在AppConfig 类中返回一个CustomLoginFilter

在该方法中添加一个CustomLoginFilter 构造函数,并使用/loginpost 参数来拦截认证。

将认证管理器设置为由authenticationManager 提供的默认值,并使用setFilterProcessesUrl 方法设置登录表单处理的URL。

覆盖onAuthenticationSuccess 方法,告诉Spring在认证成功后用户应该被重定向到哪个页面。

覆盖onAuthenticationFailure 方法,该方法告诉Spring在出现错误时用户应该被重定向到哪个页面。

private CustomLoginFilter getCustomLoginFilter() throws Exception{
        CustomLoginFilter filter = new CustomLoginFilter("/login","POST");
        filter.setAuthenticationManager(authenticationManager());
        filter.setFilterProcessesUrl("/processLogin");
        filter.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() {
            @Override
            public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                if (!response.isCommitted()){
                    response.sendRedirect("/success");
                }
            }
        });
        filter.setAuthenticationFailureHandler(new AuthenticationFailureHandler() {
            @Override
            public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
                if (!response.isCommitted()){
                    response.sendRedirect("login?error");
                }
            }
        });

        return filter;
    }

为了确保请求在认证发生之前被拦截,我们需要在AppConfig 类的configure 方法中添加addFilterBefore 方法。

addFilterBefore 方法有两个参数,由我们上面创建的过滤器和它的类组成。

 @Override
    protected void configure(HttpSecurity http) throws Exception {
         http.addFilterBefore(getCustomLoginFilter(),CustomLoginFilter.class)
    }

控制器层

登录控制器由两个GET 方法组成,一个用于返回登录表单,另一个用于在登录成功后返回成功页面。

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class LoginController {
    @GetMapping("/login")
    public String loginForm(){
        return "login";
    }

    @GetMapping("/success")
    public String successPage(){
        return "success";
    }
}

当服务器生成的分数小于0.5 ,OTP控制器会返回otp-login 页面。

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class OTPController {

    @PostMapping("/otp_login")
    public String otpForm(){
        return "otp_login";
    }

}

模板

在创建登录表单时,通过添加以下信息将ReCaptcha添加到网站中。

  • 加载javascript API。
<script src="https://www.google.com/recaptcha/api.js"></script>
  • 添加一个回调函数来处理令牌。
<script>
   function onSubmit(token) {
     document.getElementById("demo-form").submit();
   }
 </script>
  • 给你的HTML按钮添加属性。
<button class="g-recaptcha" 
        data-sitekey="reCAPTCHA_site_key" 
        data-callback='onSubmit' 
        data-action='submit'>Submit</button>

创建login.html 文件并添加下面的代码片段。

<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- Bootstrap CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">

    <title>reCAPTCHA V3 with Spring Boot</title>
    <style>
        .row-1{
            border: 2px solid green;
            background-color: sandybrown;
            color: black;
            padding: 20px;
            text-align: center;
        }

        .row-2{
            background-color: black;
            margin-top: 20px;
            color: white;
            padding: 50px;
        }
    </style>
    <script>
        function onSubmit(token){
            document.getElementById("loginForm").submit();
        }
    </script>
</head>
<body>
<div class="container">
    <div class="row row-1">
        <div class="col-md-12">
            <h1>Google reCAPTCHA V3 protected login page</h1>
        </div>

    </div>
    <div class="row row-2">
        <div class="col-md-12">
            <h1>App User Login</h1>
            <form id="loginForm" action="#" th:action="@{/processLogin}" method="post">
                <div class="mb-3">
                    <label for="username" class="form-label">username</label>
                    <input id="username" class="form-control" type="text" name="username" >
                </div>
                <div class="mb-3">
                    <label for="password" class="form-label">password</label>
                    <input id="password" class="form-control" type="password" name="password">
                </div>
               <button class="g-recaptcha btn btn-primary"
                        data-sitekey="6Lc4JNUbAAAAAE6D668IzCuXpTH5LCKypwhdBECs"
                        data-callback='onSubmit'
                        data-action="submit">
                   Login
               </button>
            </form>
        </div>

    </div>

</div>
<script src="https://www.google.com/recaptcha/api.js"></script>
</body>
</html>

创建otp_login.html 文件,并添加下面的代码片段。

<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- Bootstrap CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">

    <title>reCAPTCHA V3 with Spring Boot</title>
    <style>
        .row-1{
            border: 2px solid green;
            background-color: sandybrown;
            color: black;
            padding: 20px;
            text-align: center;
        }

        .row-2{
            background-color: black;
            margin-top: 20px;
            color: white;
            padding: 50px;
        }
    </style>
</head>
<body>
<div class="container">
    <div class="row row-1">
        <div class="col-md-12">
            <h1>Google reCAPTCHA V3 protected login page</h1>
        </div>

    </div>
    <div class="row row-2">
        <div class="col-md-12">
            <h1>OTP Login</h1>
            <form id="loginForm" action="#" th:action="@{/processLogin}" method="post">
                <div class="mb-3">
                    <label for="username" class="form-label">One Time Password (OTP)</label>
                    <input id="username" class="form-control" type="text" name="otp" >
                </div>
              <input type="submit" class="btn btn-primary" value="Login">
            </form>
        </div>

    </div>

</div>
</body>
</html>

创建success.html 文件,并添加下面的代码片段。

<!doctype html>
<html lang="en">
<head>
  <!-- Required meta tags -->
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <!-- Bootstrap CSS -->
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">

  <title>reCAPTCHA V3 with Spring Boot</title>
  <style>
    h1{
      border: 2px solid green;
      padding: 10px;
      background-color: green;
      color: white;
    }
  </style>
</head>
<body>
<div class="container">
  <div class="row">
    <div class="col-md-12">
      <h1>You have successfully logged in</h1>

    </div>

  </div>

</div>

</body>
</html>

测试

测试分数大于0.5时的登录情况

服务器通常会返回一个大于0.5的分数,这意味着认证将继续处理。登录成功后,Spring将把用户重定向到成功页面。

运行应用程序并在浏览器上导航到http://localhost:8080/login 。用CommandLineRunner 创建的用户的任何用户名和密码来填写登录细节。

login image

在按下登录按钮时,你会注意到以下g-captcha-response ,并在控制台验证后由服务器返回的结果。

larger score image

服务器生成的分数是0.9 ,如果登录成功,它将允许验证继续进入成功页面。

success page image

分数小于0.5时测试登录

如前所述,服务器生成的分数大多大于0.5 ,所以我们必须手动修改我们的应用程序,使验证方法返回的分数小于0.5 ,以达到测试目的。

在验证方法中返回一个小于0.5 的随机分数,并再次运行该应用程序。当你输入登录信息并按下登录按钮时,拦截器将返回OTP页面,这意味着生成的分数小于0.5

otp image

总结

在本教程中,你已经学会了如何在Spring Boot应用程序中实现Google ReCAPTCHA V3,方法是利用受Spring安全保护的登录表单,并以数据库中具有特定角色的持久化用户为支撑。

根据生成的分数,可以决定许多决定,正如我在介绍部分提到的,不要求用户发送电子邮件给他们的一次性密码(OTP)。