vlambda博客
学习文章列表

读书笔记《hands-on-full-stack-development-with-spring-boot-2-0-and-react》保护和测试您的后端

Chapter 4. Securing and Testing Your Backend

本章介绍如何保护和测试 Spring Boot 后端。我们将使用我们在上一章中创建的数据库应用程序作为起点。

在本章中,我们将研究以下内容:

  • How to secure your Spring Boot backend with Spring Boot
  • How to secure your Spring Boot backend with JWT
  • How to test your backend

Technical requirements


在前面章节中创建的 Spring Boot 应用程序是必要的。

Spring Security

Spring Security (https://spring.io/projects/spring-security ) 为基于 Java 的 Web 应用程序提供 security 服务。 Spring Security 项目始于 2003 年,之前被命名为 Acegi Security弹簧系统

默认情况下,Spring Security 启用以下功能:

  • An AuthenticationManager bean with an in-memory single user. The username is user and the password is printed to the console output.
  • Ignored paths for common static resource locations, such as /css, /images, and more.
  • HTTP basic security for all other endpoints.
  • Security events published to Spring ApplicationEventPublisher.
  • Common low-level features are on by default (HSTS, XSS, CSRF, and so forth).

您可以通过将以下依赖项添加到 pom.xml 文件中来在应用程序中包含 Spring Security:

 <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-security</artifactId>
 </dependency>

当你启动你的应用程序时,你可以从控制台看到 Spring Security 已经创建了一个用户名为 user 的内存用户。用户的密码可以在控制台输出中看到:

读书笔记《hands-on-full-stack-development-with-spring-boot-2-0-and-react》保护和测试您的后端

如果您向您的 API 端点发出 GET 请求,您将看到它现在是安全的,并且您将得到一个 401 Unauthorized 错误:

读书笔记《hands-on-full-stack-development-with-spring-boot-2-0-and-react》保护和测试您的后端

为了能够成功发出 GET 请求,我们必须使用基本身份验证。以下屏幕截图显示了如何使用 Postman 执行此操作。现在,通过身份验证,我们可以看到状态为 200 OK 并发送响应:

读书笔记《hands-on-full-stack-development-with-spring-boot-2-0-and-react》保护和测试您的后端

要配置 Spring Security 的行为方式,我们必须添加一个新的配置类来扩展 WebSecurityConfigurerAdapter。在您的应用程序根包中创建一个名为 SecurityConfig 的新类。以下源代码显示了安全配置类的结构。  @Configration@EnableWebSecurity 注解关闭了默认的网络安全配置,我们可以在这个类中定义我们自己的配置。在 configure(HttpSecurity http) 方法中,我们可以定义应用程序中的哪些端点是安全的,哪些不是。我们实际上还不需要这种方法,因为我们可以使用所有端点都受到保护的默认设置:

package com.packt.cardatabase;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

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

  }

}

我们还可以通过将 userDetailsS​​ervice() 方法添加到我们的 SecurityConfig 类中来将内存用户添加到我们的应用程序中。以下是该方法的源代码,它将使用用户名 user  和密码 password:

  @Bean
  @Override
  public UserDetailsService userDetailsService() {
      UserDetails user =
           User.withDefaultPasswordEncoder()
              .username("user")
              .password("password")
              .roles("USER")
              .build();

      return new InMemoryUserDetailsManager(user);
  } 

内存用户的使用在开发阶段很好,但真正的应用程序应该将用户保存在数据库中。要将用户保存到数据库中,您必须创建用户实体类和存储库。密码不应以纯文本格式保存到数据库中。 Spring Security 提供了多种哈希算法,  如 BCrypt,可用于对密码进行哈希处理。以下步骤显示了如何实现它:

  1. Create a new class called User in the domain package. Activate the domain package and right click your mouse. Select New | Class from the menu and give the name User to a new class. After that, your project structure should look like the following screenshot:
读书笔记《hands-on-full-stack-development-with-spring-boot-2-0-and-react》保护和测试您的后端
  1. Annotate the User class with the @Entity annotation. Add the class fields—ID, username, password, and role. Finally, add the constructors, getters, and setters.  We will set all fields to be nullable and that the username must be unique, by using the @Column annotation. See the following User.java source code of the fields and constructors:
package com.packt.cardatabase.domain;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(nullable = false, updatable = false)
    private Long id;

    @Column(nullable = false, unique = true)
    private String username;

    @Column(nullable = false)
    private String password;

    @Column(nullable = false)
    private String role;

    public User() {
    }

  public User(String username, String password, String role) {
    super();
    this.username = username;
    this.password = password;
    this.role = role;
  }

以下是带有 getter 和 setter 的  User.java 源代码的其余部分:

  public Long getId() {
    return id;
  }

  public void setId(Long id) {
    this.id = id;
  }

  public String getUsername() {
    return username;
  }

  public void setUsername(String username) {
    this.username = username;
  }

  public String getPassword() {
    return password;
  }

  public void setPassword(String password) {
    this.password = password;
  }

  public String getRole() {
    return role;
  }

  public void setRole(String role) {
    this.role = role;
  }
}
  1. Create a new class called UserRepository in the domain package. Activate the domain package and right click your mouse. SelectNewClass from the menu and give the name UserRepository to the new class. 
  2. The source code of the repository class is similar to what we have done in the previous chapter, but there is one query method, findByUsername, that we need in the next steps. See the following UserRepository source code:
package com.packt.cardatabase.domain;

import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends CrudRepository<User, Long> { 
    User findByUsername(String username);
}
  1. Next, we create a class that implements the UserDetailsService interface provided by Spring Security. Spring Security uses this for user authentication and authorization. Create a new package in the root package called service. Activate the root package and right click your mouse. SelectNew | Package from the menu and give the name service to a new package:
读书笔记《hands-on-full-stack-development-with-spring-boot-2-0-and-react》保护和测试您的后端

  1. Create a new class called UserDetailServiceImpl in the service package we just created. Now your project structure should look like the following:
读书笔记《hands-on-full-stack-development-with-spring-boot-2-0-and-react》保护和测试您的后端
  1. We have to inject the UserRepository class into the UserDetailServiceImpl class because that is needed to fetch the user from the database when Spring Security handles authentication. The loadByUsername method returns the UserDetails object, which is needed for authentication. Following is the source code of UserDetailServiceImpl.java:
package com.packt.cardatabase.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import com.packt.cardatabase.domain.User;
import com.packt.cardatabase.domain.UserRepository;

@Service
public class UserDetailServiceImpl implements UserDetailsService {
  @Autowired
  private UserRepository repository;


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
    { 
      User currentUser = repository.findByUsername(username);
        UserDetails user = new org.springframework.security.core
            .userdetails.User(username, currentUser.getPassword()
            , true, true, true, true, 
            AuthorityUtils.createAuthorityList(currentUser.getRole()));
        return user;
    }

}
  1. In our security configuration class, we have to define that Spring Security should use users from the database instead of in-memory users. Delete the userDetailsService() method from the SecurityConfig class to disable in-memory users. Add a new configureGlobal method to enable users from the database. We shouldn't ever save the password as plain text to the database. Therefore, we will define a password hashing algorithm in the configureGlobal method. In this example, we are using the BCrypt algorithm. This can be easily implemented with the Spring Security BCryptPasswordEncoder class. Following is the SecurityConfig.java source code. Now, the password must be hashed using BCrypt before it's saved to the database:
package com.packt.cardatabase;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import com.packt.cardatabase.service.UserDetailServiceImpl;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
  @Autowired
  private UserDetailServiceImpl userDetailsService; 

  @Autowired
  public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService)
    .passwordEncoder(new BCryptPasswordEncoder());
  }
}
  1. Finally, we can save a couple of test users to the database in our CommandLineRunner. Open the CardatabaseApplication.java file and add following code at the beginning of the class to inject UserRepository into the main class:
@Autowired 
private UserRepository urepository;
  1. Save the users to the database with hashed passwords. You can use any BCrypt calculator found on the internet:
  @Bean
  CommandLineRunner runner() {
    return args -> {
      Owner owner1 = new Owner("John" , "Johnson");
      Owner owner2 = new Owner("Mary" , "Robinson");
      orepository.save(owner1);
      orepository.save(owner2);

      repository.save(new Car("Ford", "Mustang", "Red", "ADF-1121", 
        2017, 59000, owner1));
      repository.save(new Car("Nissan", "Leaf", "White", "SSJ-3002", 
        2014, 29000, owner2));
      repository.save(new Car("Toyota", "Prius", "Silver", "KKO-0212", 
        2018, 39000, owner2));
      // username: user password: user
      urepository.save(new User("user",
      "$2a$04$1.YhMIgNX/8TkCKGFUONWO1waedKhQ5KrnB30fl0Q01QKqmzLf.Zi",
      "USER"));
      // username: admin password: admin
      urepository.save(new User("admin",
      "$2a$04$KNLUwOWHVQZVpXyMBNc7JOzbLiBjb9Tk9bP7KNcPI12ICuvzXQQKG", 
      "ADMIN"));
    };
  } 

运行应用程序后,您会看到数据库中现在有一个 user 表,并保存了两条用户记录:

读书笔记《hands-on-full-stack-development-with-spring-boot-2-0-and-react》保护和测试您的后端

现在,如果您尝试向  /api 端点没有身份验证。您应该进行身份验证才能发送成功的请求。与上一个示例的不同之处在于我们使用 来自数据库的用户进行身份验证。 

您可以使用 admin<查看对 /api端点的 GET请求/code> 用户在以下屏幕截图中:

读书笔记《hands-on-full-stack-development-with-spring-boot-2-0-and-react》保护和测试您的后端

Securing your backend using JWT

在上一节中,我们介绍了如何 使用 RESTful Web 服务的基本身份验证。当我们要使用 React 开发自己的前端时,这是不可用的。我们将使用 JSON Web Tokens (JWT)在我们的应用程序中进行身份验证。 JWT 是一种在现代 web 应用程序中实现身份验证的紧凑方式。 JWT 的体积非常小,因此可以在 URL、POST 参数或标头内发送。它还包含有关用户的所有必需信息。

JSON Web 令牌包含由点分隔的三个不同部分。第一部分是定义令牌类型和散列算法的标头。第二部分是有效负载,通常在身份验证的情况下包含有关用户的信息。第三部分是用于验证令牌在此过程中未被更改的签名。您可以看到以下 JWT 令牌示例:

eyJhbGciOiJIUzI1NiJ9.
eyJzdWIiOiJKb2UifD.
ipevRNuRP6HflG8cFKnmUPtypruRC4fc1DWtoLL62SY

下图展示了 JWT 认证过程的主要思想:

读书笔记《hands-on-full-stack-development-with-spring-boot-2-0-and-react》保护和测试您的后端

认证成功后,用户发送的请求应该始终包含认证中收到的 JWT 令牌。

我们将使用 Java JWT 库(https://github.com/jwtk/jjwt),是Java和Android的JSON Web Token库;因此,我们必须将以下依赖项添加到 pom.xml 文件中。 JWT 库用于创建和解析 JWT 令牌:

<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt</artifactId>
  <version>0.9.0</version>
</dependency>

以下步骤显示了如何在我们的后端启用 JWT 身份验证:

  1.  Create a new class called AuthenticationService in the service package. In the beginning of the class we will define a few constants; EXPIRATIONTIME defines the expiration time of the token in milliseconds. SIGNINGKEY is an algorithm-specific signing key used to digitally sign the JWT. You should use a base64 encoded string. PREFIX defines the prefix of the token and the Bearer schema is typically used. The addToken method creates the token and adds it to the request's Authorization header. The signing key is encoded using the SHA-512 algorithm. The method also adds Access-Control-Expose-Headers  to the header with the Authorization value. This is needed because we are not able to access the Authorization header through a JavaScript frontend by default. The getAuthentication method gets the token from the response Authorization header using the parser() method provided by the jjwt library. The whole AuthenticationService source code can be seen here:
package com.packt.cardatabase.service;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Date;

import static java.util.Collections.emptyList;

public class AuthenticationService {
  static final long EXPIRATIONTIME = 864_000_00; // 1 day in milliseconds
  static final String SIGNINGKEY = "SecretKey";
  static final String PREFIX = "Bearer";

  // Add token to Authorization header
  static public void addToken(HttpServletResponse res, String username) {
    String JwtToken = Jwts.builder().setSubject(username)
        .setExpiration(new Date(System.currentTimeMillis() 
            + EXPIRATIONTIME))
        .signWith(SignatureAlgorithm.HS512, SIGNINGKEY)
        .compact();
    res.addHeader("Authorization", PREFIX + " " + JwtToken);
  res.addHeader("Access-Control-Expose-Headers", "Authorization");
  }

  // Get token from Authorization header
  static public Authentication getAuthentication(HttpServletRequest request) {
    String token = request.getHeader("Authorization");
    if (token != null) {
      String user = Jwts.parser()
          .setSigningKey(SIGNINGKEY)
          .parseClaimsJws(token.replace(PREFIX, ""))
          .getBody()
          .getSubject();

      if (user != null) 
        return new UsernamePasswordAuthenticationToken(user, null,
            emptyList());
    }
    return null;
  }
}
  1. Next, we will add a new simple POJO class to keep credentials for authentication. Create a new class called AccountCredentials in the domain package. The class has two fields—username and password. The following is the source code of the class. This class doesn't have the @Entity annotation because we don't have to save credentials to the database:
package com.packt.cardatabase.domain;

public class AccountCredentials {
  private String username;
  private String password;

  public String getUsername() {
    return username;
  }
  public void setUsername(String username) {
    this.username = username;
  }
  public String getPassword() {
    return password;
  }
  public void setPassword(String password) {
    this.password = password;
  } 
}
  1. We will use filter classes for login and authentication. Create a new class called LoginFilter  in the root package that handles POST requests to the /login endpoint. The LoginFilter class extends the Spring Security AbstractAuthenticationProcessingFilter, which requires that you set the authenticationManager property. Authentication is performed by the attemptAuthentication method. If the authentication is successful, the succesfulAuthentication method is executed. This method will then call the addToken method in our service class and the token will be added to the Authorization header:
package com.packt.cardatabase;

import java.io.IOException;
import java.util.Collections;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.packt.cardatabase.domain.AccountCredentials;
import com.packt.cardatabase.service.AuthenticationService;

public class LoginFilter extends AbstractAuthenticationProcessingFilter {

  public LoginFilter(String url, AuthenticationManager authManager) {
    super(new AntPathRequestMatcher(url));
    setAuthenticationManager(authManager);
  }

  @Override
  public Authentication attemptAuthentication(
  HttpServletRequest req, HttpServletResponse res)
      throws AuthenticationException, IOException, ServletException {
  AccountCredentials creds = new ObjectMapper()
        .readValue(req.getInputStream(), AccountCredentials.class);
  return getAuthenticationManager().authenticate(
        new UsernamePasswordAuthenticationToken(
            creds.getUsername(),
            creds.getPassword(),
            Collections.emptyList()
        )
    );
  }

  @Override
  protected void successfulAuthentication(
      HttpServletRequest req,
      HttpServletResponse res, FilterChain chain,
      Authentication auth) throws IOException, ServletException {
    AuthenticationService.addToken(res, auth.getName());
  }
}
  1. Create a new class called AuthenticationFilter in the root package. The class extends GenericFilterBean, which is a generic superclass for any type of filter. This class will handle authentication in all other endpoints except /login. The AuthenticationFilter uses the addAuthentication method from our service class to get a token from the request Authorization header:
package com.packt.cardatabase;

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;

import com.packt.cardatabase.service.AuthenticationService;

public class AuthenticationFilter extends GenericFilterBean {
  @Override
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
    Authentication authentication = AuthenticationService.getAuthentication((HttpServletRequest)request);

    SecurityContextHolder.getContext().
        setAuthentication(authentication);
    filterChain.doFilter(request, response);
  }
}
  1. Finally, we have to make changes to our SecurityConfig class's configure method. There, we define that the POST method request to the /login endpoint is allowed without authentication and that requests to all other endpoints need authentication. We also define the filters to be used in the /login and other endpoints by using the addFilterBefore method:
  //SecurityConfig.java  
  @Override
    protected void configure(HttpSecurity http) throws Exception {
     http.cors().and().authorizeRequests()
      .antMatchers(HttpMethod.POST, "/login").permitAll()
          .anyRequest().authenticated()
          .and()
          // Filter for the api/login requests
          .addFilterBefore(new LoginFilter("/login",
           authenticationManager()),
                  UsernamePasswordAuthenticationFilter.class)
          // Filter for other requests to check JWT in header
          .addFilterBefore(new AuthenticationFilter(),
                  UsernamePasswordAuthenticationFilter.class);
    }
  1. We will also add a CORS (Cross-Origin Resource Sharing) filter in our security configuration class. This is needed for the frontend, that is sending requests from the other origin. The CORS filter intercepts requests, and if these are identified as cross origin, it adds proper headers to the request. For that, we will use Spring Security's CorsConfigurationSource interface. In this example, we will allow all HTTP methods and headers. You can define the list of allowed origins, methods, and headers here, if you need more finely graded definition. Add the following source into your SecurityConfig class to enable the CORS filter:
  // SecurityConfig.java  
  @Bean
    CorsConfigurationSource corsConfigurationSource() {
        UrlBasedCorsConfigurationSource source = 
            new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOrigins(Arrays.asList("*"));
        config.setAllowedMethods(Arrays.asList("*"));
        config.setAllowedHeaders(Arrays.asList("*"));
        config.setAllowCredentials(true);
        config.applyPermitDefaultValues();

        source.registerCorsConfiguration("/**", config);
        return source;
  } 

现在,在您运行应用程序之后,我们可以使用 POST 方法调用 /login 端点,在这种情况下成功登录后,我们将在 Authorization 标头中收到 JWT 令牌:

读书笔记《hands-on-full-stack-development-with-spring-boot-2-0-and-react》保护和测试您的后端

登录成功后,我们可以通过发送在授权 标头。请参阅以下屏幕截图中的示例:

读书笔记《hands-on-full-stack-development-with-spring-boot-2-0-and-react》保护和测试您的后端

现在,我们的后端已经实现了所有需要的功能。接下来,我们将继续进行后端单元测试。

Testing in Spring Boot

Spring Boot 测试启动器 package 被 Spring 添加到 pom.xml我们创建项目时的 Initializr。这是自动添加的,无需在 Spring Initializr 页面中进行任何选择:

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>

Spring Boot 测试启动器提供了许多方便的测试库,例如 JUnit、Mockito、AssertJ 等。如果您看一下,您的项目结构已经为测试类创建了自己的包:

读书笔记《hands-on-full-stack-development-with-spring-boot-2-0-and-react》保护和测试您的后端

默认情况下,Spring Boot 使用内存数据库进行测试。我们现在使用的是 MariaDB,但也可以通过将以下依赖项添加到  pom.xml 文件中来使用 H2 进行测试。范围定义 H2 数据库将仅用于运行测试;否则,应用程序将使用 MariaDB 数据库:

    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>test</scope>
    </dependency> 

如果还想使用默认数据库进行测试,可以使用 @AutoConfigureTestDatabase注解。

Creating unit tests

对于单元测试,我们使用 JUnit,它是一个流行的基于 Java 的单元测试库。以下源代码显示了 Spring Boot 测试类的示例框架。  @SpringBootTest 注释指定 该类是常规测试类运行基于 Spring Boot 的测试。  @Test 方法之前的注释向 JUnit 定义了该方法可以作为测试用例运行。  @RunWith(SpringRunner.class) 注释提供 Spring ApplicationContext 并将 bean 注入到您的测试实例中:

@RunWith(SpringRunner.class)
@SpringBootTest
public class MyTestsClass {

  @Test
  public void testMethod() {
    ...
  }

}

首先,我们将创建我们的第一个测试用例,它将在创建任何正式的测试用例之前测试您的应用程序的主要功能。打开已经为您的应用程序创建的 CardatabaseApplicationTest 测试类。有一种称为 contextLoads 的测试方法,我们将在其中添加测试。以下测试检查控制器实例是否已成功创建和注入:

package com.packt.cardatabase;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import com.packt.cardatabase.web.CarController;

@RunWith(SpringRunner.class)
@SpringBootTest
public class CardatabaseApplicationTests {
  @Autowired
  private CarController controller;

  @Test
  public void contextLoads() {
    assertThat(controller).isNotNull();
  }

}

要在 Eclipse 中运行测试,请在 Project Explorer 中激活测试类并右键单击鼠标。选择运行 作为 | JUnit 测试 从菜单中。您现在应该在 Eclipse 工作台的下部看到 JUnit 选项卡。测试结果显示在此选项卡中,并且测试用例已通过:

读书笔记《hands-on-full-stack-development-with-spring-boot-2-0-and-react》保护和测试您的后端

接下来,我们将为我们的汽车存储库创建单元测试以测试 CRUD 操作。在根测试包中创建一个名为 CarRepositoryTest 的新类。如果测试只关注 JPA 组件,则可以使用 @DataJpaTest 代替 @SpringBootTest注解。使用此注解时,会自动配置 H2 数据库、Hibernate 和 Spring Data 以进行测试。 SQL 日志记录也将打开。 测试 默认情况下是事务性的,并在测试用例结束时回滚。  TestEntityManager 用于处理持久实体,旨在用于测试。您可以在下面看到 JPA 测试类骨架的源代码:

package com.packt.cardatabase;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import org.springframework.test.context.junit4.SpringRunner;

import com.packt.cardatabase.domain.Car;
import com.packt.cardatabase.domain.CarRepository;

@RunWith(SpringRunner.class)
@DataJpaTest
public class CarRepositoryTest {
  @Autowired
  private TestEntityManager entityManager;

  @Autowired
  private CarRepository repository;

   // Test cases..
}

我们将添加第一个测试用例来测试将新车添加到数据库中。使用 提供的 persistAndFlush 方法创建一个新的 car 对象并保存到数据库中测试实体管理器。然后,如果保存成功,我们检查汽车 ID 不能为空。以下源代码显示了测试用例方法。将以下方法代码添加到您的 CarRepositoryTest 类中:

  @Test
  public void saveCar() {
    Car car = new Car("Tesla", "Model X", "White", "ABC-1234",
        2017, 86000);
    entityManager.persistAndFlush(car);

    assertThat(car.getId()).isNotNull();
  }

第二个测试用例将测试从数据库中删除汽车。一个新的 car 对象被创建并保存到数据库中。然后,从数据库中删除所有汽车,最后 findAll() 查询方法应该返回一个空列表。以下源代码显示了测试用例方法。将以下方法代码添加到您的 CarRepositoryTest 类中:

  @Test
  public void deleteCars() {
    entityManager.persistAndFlush(new Car("Tesla", "Model X", "White",
        "ABC-1234", 2017, 86000));
    entityManager.persistAndFlush(new Car("Mini", "Cooper", "Yellow",
        "BWS-3007", 2015, 24500));

    repository.deleteAll();
    assertThat(repository.findAll()).isEmpty();
  } 

运行测试用例并在 Eclipse JUnit 选项卡上检查测试是否通过:

读书笔记《hands-on-full-stack-development-with-spring-boot-2-0-and-react》保护和测试您的后端

接下来,我们将展示如何测试您的 RESTful Web 服务 JWT 身份验证功能。为了测试控制器或任何暴露的端点,我们可以使用 MockMvc。通过使用 MockMvc,服务器没有启动,但是tests在 Spring 处理 HTTP 请求的层中执行,因此它模拟真实情况。 MockMvc 提供了 执行发送请求的方法。要测试身份验证,我们必须将凭据添加到请求正文。我们执行两个请求;第一个具有正确的凭据,我们检查状态是否为 OK。第二个请求包含不正确的凭据,我们检查是否收到 4XX HTTP 错误:

package com.packt.cardatabase;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;

import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class CarRestTest {
  @Autowired
    private MockMvc mockMvc;

  @Test
  public void testAuthentication() throws Exception {
    // Testing authentication with correct credentials
        this.mockMvc.perform(post("/login")
          .content("{\"username\":\"admin\", \"password\":\"admin\"}")).
          andDo(print()).andExpect(status().isOk());

    // Testing authentication with wrong credentials
        this.mockMvc.perform(post("/login")
          .content("{\"username\":\"admin\", \"password\":\"wrongpwd\"}")).
          andDo(print()).andExpect(status().is4xxClientError());

  }

}

现在,当我们运行身份验证测试时,我们可以看到测试通过了:

读书笔记《hands-on-full-stack-development-with-spring-boot-2-0-and-react》保护和测试您的后端

现在,我们已经介绍了 Spring Boot 应用程序中的测试基础知识,您应该具备为您的应用程序实现更多测试用例所需的知识。

Summary


在本章中,我们专注于保护和测试 Spring Boot 后端。保护首先是使用 Spring Security 完成的。前端将在接下来的章节中使用 React 开发;因此,我们实现了 JWT 认证,这是一种适合我们需求的轻量级认证方式。我们还介绍了测试 Spring Boot 应用程序的基础知识。我们使用 JUnit 进行单元测试,并为 JPA 和 RESTful Web 服务身份验证实现了测试用例。在下一章中,我们将为前端开发设置环境和工具。

Questions


  1. What is Spring Security?
  2. How can you secure your backend with Spring Boot?
  3. What is JWT?
  4. How can you secure your backend with JWT?
  5. How can you create unit tests with Spring Boot?
  6. How can you run and check the results of unit tests?