Spring Boot RESTful API 开发与测试
Spring Boot RESTful API 的开发流程与测试方法。涵盖 RESTful 定义、HTTP 方法、状态码,以及使用 Spring Boot 构建 CRUD 接口的步骤。详细讲解了单元测试、集成测试和 Mock 测试的实现方式。此外,还深入探讨了基于 Spring Security 和 JWT 的认证授权方案,并提供了实际应用场景示例,帮助开发者掌握企业级 API 开发的核心技能。

Spring Boot RESTful API 的开发流程与测试方法。涵盖 RESTful 定义、HTTP 方法、状态码,以及使用 Spring Boot 构建 CRUD 接口的步骤。详细讲解了单元测试、集成测试和 Mock 测试的实现方式。此外,还深入探讨了基于 Spring Security 和 JWT 的认证授权方案,并提供了实际应用场景示例,帮助开发者掌握企业级 API 开发的核心技能。

学习目标:掌握 Spring Boot RESTful API 开发与测试的核心概念与使用方法,包括 RESTful API 的定义与特点、Spring Boot RESTful API 的开发、Spring Boot RESTful API 的测试、Spring Boot RESTful API 的认证与授权、Spring Boot RESTful API 的实际应用场景,学会在实际开发中处理 RESTful API 问题。
重点:
RESTful API 是 Java 开发中的主流 API 设计风格。
定义:RESTful API 是一种基于 REST 架构风格的 API 设计。
作用:
REST 架构风格的特点:
结论:RESTful API 是一种基于 REST 架构风格的 API 设计,作用是实现 Web 应用的 API 设计、提高开发效率、提供统一的编程模型。
定义:RESTful API 的常用 HTTP 方法是指 RESTful API 使用的 HTTP 请求方法。
方法:
常用 HTTP 响应状态码:
结论:RESTful API 的常用 HTTP 方法包括 GET、POST、PUT、DELETE、PATCH,常用 HTTP 响应状态码包括 200、201、400、401、403、404、500。
Spring Boot RESTful API 的开发是 Java 开发中的重要内容。
定义:开发 RESTful API 的步骤是指使用 Spring Boot 开发 RESTful API 的方法。
步骤:
示例:
pom.xml 文件中的依赖:
<dependencies>
<!-- Web 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Data JPA 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- H2 数据库依赖 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
实体类:
import javax.persistence.*;
@Entity
@Table(name = "product")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String productId;
private String productName;
private double price;
private int sales;
public Product() {}
public Product(String productId, String productName, double price, int sales) {
this.productId = productId;
this.productName = productName;
this.price = price;
this.sales = sales;
}
// Getter 和 Setter 方法
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getProductId() { return productId; }
public void setProductId(String productId) { this.productId = productId; }
public String getProductName() { return productName; }
public void setProductName(String productName) { this.productName = productName; }
public double getPrice() { return price; }
public void setPrice(double price) { this.price = price; }
public int getSales() { return sales; }
public void setSales(int sales) { this.sales = sales; }
@Override
public String toString() {
return "Product{" + "id=" + id + ", productId='" + productId + '\'' + ", productName='" + productName + '\'' + ", price=" + price + ", sales=" + sales + '}';
}
}
Repository 接口:
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
List<Product> findBySalesGreaterThan(int sales);
}
Service 类:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
@Transactional
public void addProduct(Product product) {
productRepository.save(product);
}
@Transactional
public void updateProduct(Product product) {
productRepository.save(product);
}
@Transactional
public void deleteProduct(Long id) {
productRepository.deleteById(id);
}
@Transactional(readOnly = true)
public List<Product> getAllProducts() {
return productRepository.findAll();
}
@Transactional(readOnly = true)
public List<Product> getTopSellingProducts(int topN) {
List<Product> products = productRepository.findBySalesGreaterThan(0);
products.sort((p1, p2) -> p2.getSales() - p1.getSales());
if (products.size() > topN) {
return products.subList(0, topN);
}
return products;
}
}
Controller 类:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/products")
public class ProductController {
@Autowired
private ProductService productService;
@GetMapping("/")
public ResponseEntity<List<Product>> getAllProducts() {
List<Product> products = productService.getAllProducts();
return new ResponseEntity<>(products, HttpStatus.OK);
}
@PostMapping("/")
public ResponseEntity<Void> addProduct(@RequestBody Product product) {
productService.addProduct(product);
return new ResponseEntity<>(HttpStatus.CREATED);
}
@PutMapping("/{id}")
public ResponseEntity<Void> updateProduct(@PathVariable Long id, @RequestBody Product product) {
product.setId(id);
productService.updateProduct(product);
return new ResponseEntity<>(HttpStatus.OK);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
productService.deleteProduct(id);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
@GetMapping("/top-selling")
public ResponseEntity<List<Product>> getTopSellingProducts(@RequestParam int topN) {
List<Product> products = productService.getTopSellingProducts(topN);
return new ResponseEntity<>(products, HttpStatus.OK);
}
}
测试类:
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ProductApplicationTests {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@Test
void contextLoads() {}
@Test
void testGetAllProducts() {
List<Product> products = restTemplate.getForObject("http://localhost:" + port + "/api/products/", List.class);
assertThat(products).hasSize(5);
}
@Test
void testAddProduct() {
Product product = new Product("P006", "平板", 2000.0, 70);
restTemplate.postForEntity("http://localhost:" + port + "/api/products/", product, Void.class);
List<Product> products = restTemplate.getForObject("http://localhost:" + port + "/api/products/", List.class);
assertThat(products).hasSize(6);
}
@Test
void testUpdateProduct() {
Product product = new Product("P001", "手机", 1500.0, 120);
restTemplate.put("http://localhost:" + port + "/api/products/1", product);
List<Product> products = restTemplate.getForObject("http://localhost:" + port + "/api/products/", List.class);
assertThat(products.get(0).getPrice()).isEqualTo(1500.0);
}
@Test
void testDeleteProduct() {
restTemplate.delete("http://localhost:" + port + "/api/products/2");
List<Product> products = restTemplate.getForObject("http://localhost:" + port + "/api/products/", List.class);
assertThat(products).hasSize(4);
}
@Test
void testGetTopSellingProducts() {
List<Product> topSellingProducts = restTemplate.getForObject("http://localhost:" + port + "/api/products/top-selling?topN=3", List.class);
assertThat(topSellingProducts).hasSize(3);
assertThat(topSellingProducts.get(0).getProductId()).isEqualTo("P004");
assertThat(topSellingProducts.get(1).getProductId()).isEqualTo("P005");
assertThat(topSellingProducts.get(2).getProductId()).isEqualTo("P001");
}
}
结论:开发 RESTful API 的步骤包括创建 Spring Boot 项目、添加所需的依赖、创建实体类、创建 Repository 接口、创建 Service 类、创建 Controller 类、测试应用。
Spring Boot RESTful API 的测试是 Java 开发中的重要内容。
定义:单元测试是指测试单个方法或类的功能。
常用注解:
示例:
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
class ProductServiceTests {
@Autowired
private ProductService productService;
@Test
void testAddProduct() {
Product product = new Product("P006", "平板", 2000.0, 70);
productService.addProduct(product);
List<Product> products = productService.getAllProducts();
assertThat(products).hasSize(6);
}
@Test
void testUpdateProduct() {
Product product = new Product("P001", "手机", 1500.0, 120);
product.setId(1L);
productService.updateProduct(product);
List<Product> products = productService.getAllProducts();
assertThat(products.get(0).getPrice()).isEqualTo(1500.0);
}
@Test
void testDeleteProduct() {
productService.deleteProduct(2L);
List<Product> products = productService.getAllProducts();
assertThat(products).hasSize(4);
}
@Test
void testGetTopSellingProducts() {
List<Product> topSellingProducts = productService.getTopSellingProducts(3);
assertThat(topSellingProducts).hasSize(3);
assertThat(topSellingProducts.get(0).getProductId()).isEqualTo("P004");
assertThat(topSellingProducts.get(1).getProductId()).isEqualTo("P005");
assertThat(topSellingProducts.get(2).getProductId()).isEqualTo("P001");
}
}
结论:单元测试是指测试单个方法或类的功能,常用注解包括 @SpringBootTest、@Test、@Autowired。
定义:集成测试是指测试多个组件之间的交互。
常用注解:
示例:
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ProductControllerTests {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@Test
void testGetAllProducts() {
List<Product> products = restTemplate.getForObject("http://localhost:" + port + "/api/products/", List.class);
assertThat(products).hasSize(5);
}
@Test
void testAddProduct() {
Product product = new Product("P006", "平板", 2000.0, 70);
restTemplate.postForEntity("http://localhost:" + port + "/api/products/", product, Void.class);
List<Product> products = restTemplate.getForObject("http://localhost:" + port + "/api/products/", List.class);
assertThat(products).hasSize(6);
}
@Test
void testUpdateProduct() {
Product product = new Product("P001", "手机", 1500.0, 120);
restTemplate.put("http://localhost:" + port + "/api/products/1", product);
List<Product> products = restTemplate.getForObject("http://localhost:" + port + "/api/products/", List.class);
assertThat(products.get(0).getPrice()).isEqualTo(1500.0);
}
@Test
void testDeleteProduct() {
restTemplate.delete("http://localhost:" + port + "/api/products/2");
List<Product> products = restTemplate.getForObject("http://localhost:" + port + "/api/products/", List.class);
assertThat(products).hasSize(4);
}
@Test
void testGetTopSellingProducts() {
List<Product> topSellingProducts = restTemplate.getForObject("http://localhost:" + port + "/api/products/top-selling?topN=3", List.class);
assertThat(topSellingProducts).hasSize(3);
assertThat(topSellingProducts.get(0).getProductId()).isEqualTo("P004");
assertThat(topSellingProducts.get(1).getProductId()).isEqualTo("P005");
assertThat(topSellingProducts.get(2).getProductId()).isEqualTo("P001");
}
}
结论:集成测试是指测试多个组件之间的交互,常用注解包括 @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)、@LocalServerPort、@Autowired。
定义:Mock 测试是指模拟对象的行为。
常用注解:
示例:
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import java.util.Arrays;
import java.util.List;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest(ProductController.class)
class ProductControllerMockTests {
@Autowired
private MockMvc mockMvc;
@MockBean
private ProductService productService;
@Test
void testGetAllProducts() throws Exception {
List<Product> products = Arrays.asList(
new Product("P001", "手机", 1000.0, 100),
new Product("P002", "电脑", 5000.0, 50),
new Product("P003", "电视", 3000.0, 80),
new Product("P004", "手表", 500.0, 200),
new Product("P005", "耳机", 300.0, 150)
);
when(productService.getAllProducts()).thenReturn(products);
mockMvc.perform(get("/api/products/")).andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$[0].productId").value("P001"))
.andExpect(jsonPath("$[1].productId").value("P002"))
.andExpect(jsonPath("$[2].productId").value("P003"))
.andExpect(jsonPath("$[3].productId").value("P004"))
.andExpect(jsonPath("$[4].productId").value("P005"));
verify(productService, times(1)).getAllProducts();
}
@Test
void testAddProduct() throws Exception {
Product product = new Product("P006", "平板", 2000.0, 70);
doNothing().when(productService).addProduct(any(Product.class));
mockMvc.perform(post("/api/products/").contentType(MediaType.APPLICATION_JSON)
.content("{\"productId\":\"P006\",\"productName\":\"平板\",\"price\":2000.0,\"sales\":70}"))
.andExpect(status().isCreated());
verify(productService, times(1)).addProduct(any(Product.class));
}
@Test
void testUpdateProduct() throws Exception {
Product product = new Product("P001", "手机", 1500.0, 120);
doNothing().when(productService).updateProduct(any(Product.class));
mockMvc.perform(put("/api/products/1").contentType(MediaType.APPLICATION_JSON)
.content("{\"id\":1,\"productId\":\"P001\",\"productName\":\"手机\",\"price\":1500.0,\"sales\":120}"))
.andExpect(status().isOk());
verify(productService, times(1)).updateProduct(any(Product.class));
}
@Test
void testDeleteProduct() throws Exception {
doNothing().when(productService).deleteProduct(anyLong());
mockMvc.perform(delete("/api/products/2")).andExpect(status().isNoContent());
verify(productService, times(1)).deleteProduct(anyLong());
}
@Test
void testGetTopSellingProducts() throws Exception {
List<Product> topSellingProducts = Arrays.asList(
new Product("P004", "手表", 500.0, 200),
new Product("P005", "耳机", 300.0, 150),
new Product("P001", "手机", 1000.0, 100)
);
when(productService.getTopSellingProducts(3)).thenReturn(topSellingProducts);
mockMvc.perform(get("/api/products/top-selling?topN=3")).andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$[0].productId").value("P004"))
.andExpect(jsonPath("$[1].productId").value("P005"))
.andExpect(jsonPath("$[2].productId").value("P001"));
verify(productService, times(1)).getTopSellingProducts(3);
}
}
结论:Mock 测试是指模拟对象的行为,常用注解包括 @WebMvcTest、@MockBean、@Autowired。
Spring Boot RESTful API 的认证与授权是 Java 开发中的重要内容。
定义:Spring Security 是 Spring Boot 提供的安全框架。
作用:
示例: pom.xml 文件中的 Spring Security 依赖:
<dependencies>
<!-- Web 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Data JPA 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- H2 数据库依赖 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Spring Security 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Spring Security 配置类:
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.password.NoOpPasswordEncoder;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.passwordEncoder(NoOpPasswordEncoder.getInstance())
.withUser("admin").password("admin123").roles("ADMIN")
.and()
.withUser("user").password("user123").roles("USER");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/api/products/top-selling").hasRole("ADMIN")
.antMatchers("/api/products/**").hasRole("USER")
.and().httpBasic();
}
}
测试类:
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import java.util.Base64;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ProductControllerSecurityTests {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@Test
void testGetAllProductsWithoutAuthentication() {
ResponseEntity<List> response = restTemplate.getForEntity("http://localhost:" + port + "/api/products/", List.class);
assertThat(response.getStatusCodeValue()).isEqualTo(401);
}
@Test
void testGetAllProductsWithUserAuthentication() {
String credentials = "user:user123";
String base64Credentials = Base64.getEncoder().encodeToString(credentials.getBytes());
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Basic " + base64Credentials);
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<List> response = restTemplate.exchange("http://localhost:" + port + "/api/products/", HttpMethod.GET, entity, List.class);
assertThat(response.getStatusCodeValue()).isEqualTo(200);
assertThat(response.getBody()).hasSize(5);
}
@Test
void testGetTopSellingProductsWithUserAuthentication() {
String credentials = "user:user123";
String base64Credentials = Base64.getEncoder().encodeToString(credentials.getBytes());
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Basic " + base64Credentials);
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<List> response = restTemplate.exchange("http://localhost:" + port + "/api/products/top-selling?topN=3", HttpMethod.GET, entity, List.class);
assertThat(response.getStatusCodeValue()).isEqualTo(403);
}
@Test
void testGetTopSellingProductsWithAdminAuthentication() {
String credentials = "admin:admin123";
String base64Credentials = Base64.getEncoder().encodeToString(credentials.getBytes());
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Basic " + base64Credentials);
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<List> response = restTemplate.exchange("http://localhost:" + port + "/api/products/top-selling?topN=3", HttpMethod.GET, entity, List.class);
assertThat(response.getStatusCodeValue()).isEqualTo(200);
assertThat(response.getBody()).hasSize(3);
}
}
结论:Spring Security 是 Spring Boot 提供的安全框架,作用是实现用户认证、用户授权、提供安全的编程模型。
定义:JWT 是一种基于 JSON 的开放标准,用于在网络应用之间安全地传输信息。
作用:
示例: pom.xml 文件中的 JWT 依赖:
<dependencies>
<!-- Web 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Data JPA 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- H2 数据库依赖 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Spring Security 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT 依赖 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
JWT 工具类:
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
private Claims extractAllClaims(String token) {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}
private Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
public String generateToken(String username) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, username);
}
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
.signWith(SignatureAlgorithm.HS256, secret)
.compact();
}
public Boolean validateToken(String token, String username) {
final String extractedUsername = extractUsername(token);
return (extractedUsername.equals(username) && !isTokenExpired(token));
}
}
JWT 过滤器:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
final String authorizationHeader = request.getHeader("Authorization");
String username = null;
String jwt = null;
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
jwt = authorizationHeader.substring(7);
username = jwtUtil.extractUsername(jwt);
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(jwt, userDetails.getUsername())) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
chain.doFilter(request, response);
}
}
Spring Security 配置类:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtRequestFilter jwtRequestFilter;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/authenticate").permitAll()
.anyRequest().authenticated()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}
}
认证控制器:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.web.bind.annotation.*;
@RestController
public class JwtAuthenticationController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtUtil jwtTokenUtil;
@Autowired
private UserDetailsService userDetailsService;
@PostMapping("/authenticate")
public ResponseEntity<?> createAuthenticationToken(@RequestBody JwtRequest authenticationRequest) throws Exception {
try {
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(authenticationRequest.getUsername(), authenticationRequest.getPassword()));
} catch (BadCredentialsException e) {
throw new Exception("Incorrect username or password", e);
}
final UserDetails userDetails = userDetailsService.loadUserByUsername(authenticationRequest.getUsername());
final String jwt = jwtTokenUtil.generateToken(userDetails.getUsername());
return ResponseEntity.ok(new JwtResponse(jwt));
}
}
JwtRequest 类:
public class JwtRequest {
private String username;
private String password;
public JwtRequest() {}
public JwtRequest(String username, String password) {
this.username = username;
this.password = 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; }
}
JwtResponse 类:
public class JwtResponse {
private final String jwt;
public JwtResponse(String jwt) {
this.jwt = jwt;
}
public String getJwt() { return jwt; }
}
用户服务类:
import org.springframework.security.core.GrantedAuthority;
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.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class MyUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if ("user".equals(username)) {
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
return new User("user", "user123", authorities);
} else if ("admin".equals(username)) {
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
return new User("admin", "admin123", authorities);
} else {
throw new UsernameNotFoundException("User not found with username: " + username);
}
}
}
应用配置文件(application.properties):
# 服务器端口
server.port=8080
# 数据库连接信息
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
# JPA 配置
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
# H2 数据库控制台
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
# JWT 配置
jwt.secret=mysecret
jwt.expiration=3600
测试类:
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class JwtAuthenticationControllerTests {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@Test
void testAuthenticateUser() {
JwtRequest request = new JwtRequest("user", "user123");
ResponseEntity<JwtResponse> response = restTemplate.postForEntity("http://localhost:" + port + "/authenticate", request, JwtResponse.class);
assertThat(response.getStatusCodeValue()).isEqualTo(200);
assertThat(response.getBody().getJwt()).isNotNull();
}
@Test
void testAuthenticateAdmin() {
JwtRequest request = new JwtRequest("admin", "admin123");
ResponseEntity<JwtResponse> response = restTemplate.postForEntity("http://localhost:" + port + "/authenticate", request, JwtResponse.class);
assertThat(response.getStatusCodeValue()).isEqualTo(200);
assertThat(response.getBody().getJwt()).isNotNull();
}
@Test
void testAuthenticateInvalidUser() {
JwtRequest request = new JwtRequest("invalid", "invalid123");
ResponseEntity<JwtResponse> response = restTemplate.postForEntity("http://localhost:" + port + "/authenticate", request, JwtResponse.class);
assertThat(response.getStatusCodeValue()).isEqualTo(401);
}
@Test
void testGetAllProductsWithUserJwt() {
JwtRequest request = new JwtRequest("user", "user123");
ResponseEntity<JwtResponse> authResponse = restTemplate.postForEntity("http://localhost:" + port + "/authenticate", request, JwtResponse.class);
String token = authResponse.getBody().getJwt();
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Bearer " + token);
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<Map> response = restTemplate.exchange("http://localhost:" + port + "/api/products/", HttpMethod.GET, entity, Map.class);
assertThat(response.getStatusCodeValue()).isEqualTo(200);
}
@Test
void testGetTopSellingProductsWithAdminJwt() {
JwtRequest request = new JwtRequest("admin", "admin123");
ResponseEntity<JwtResponse> authResponse = restTemplate.postForEntity("http://localhost:" + port + "/authenticate", request, JwtResponse.class);
String token = authResponse.getBody().getJwt();
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Bearer " + token);
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<Map> response = restTemplate.exchange("http://localhost:" + port + "/api/products/top-selling?topN=3", HttpMethod.GET, entity, Map.class);
assertThat(response.getStatusCodeValue()).isEqualTo(200);
}
}
结论:JWT 是一种基于 JSON 的开放标准,作用是实现用户认证、用户授权、提供安全的编程模型。
在实际开发中,Spring Boot RESTful API 的应用场景非常广泛,如:
示例:
产品类:
import javax.persistence.*;
@Entity
@Table(name = "product")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String productId;
private String productName;
private double price;
private int sales;
public Product() {}
public Product(String productId, String productName, double price, int sales) {
this.productId = productId;
this.productName = productName;
this.price = price;
this.sales = sales;
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getProductId() { return productId; }
public void setProductId(String productId) { this.productId = productId; }
public String getProductName() { return productName; }
public void setProductName(String productName) { this.productName = productName; }
public double getPrice() { return price; }
public void setPrice(double price) { this.price = price; }
public int getSales() { return sales; }
public void setSales(int sales) { this.sales = sales; }
@Override
public String toString() {
return "Product{" + "id=" + id + ", productId='" + productId + '\'' + ", productName='" + productName + '\'' + ", price=" + price + ", sales=" + sales + '}';
}
}
产品 Repository:
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
List<Product> findBySalesGreaterThan(int sales);
}
产品 Service:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
@Transactional
public void addProduct(Product product) {
productRepository.save(product);
}
@Transactional
public void updateProduct(Product product) {
productRepository.save(product);
}
@Transactional
public void deleteProduct(Long id) {
productRepository.deleteById(id);
}
@Transactional(readOnly = true)
public List<Product> getAllProducts() {
return productRepository.findAll();
}
@Transactional(readOnly = true)
public List<Product> getTopSellingProducts(int topN) {
List<Product> products = productRepository.findBySalesGreaterThan(0);
products.sort((p1, p2) -> p2.getSales() - p1.getSales());
if (products.size() > topN) {
return products.subList(0, topN);
}
return products;
}
}
产品控制器:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/products")
public class ProductController {
@Autowired
private ProductService productService;
@GetMapping("/")
public List<Product> getAllProducts() {
return productService.getAllProducts();
}
@PostMapping("/")
public void addProduct(@RequestBody Product product) {
productService.addProduct(product);
}
@PutMapping("/{id}")
public void updateProduct(@PathVariable Long id, @RequestBody Product product) {
product.setId(id);
productService.updateProduct(product);
}
@DeleteMapping("/{id}")
public void deleteProduct(@PathVariable Long id) {
productService.deleteProduct(id);
}
@GetMapping("/top-selling")
public List<Product> getTopSellingProducts(@RequestParam int topN) {
return productService.getTopSellingProducts(topN);
}
}
应用启动类:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ProductApplication {
public static void main(String[] args) {
SpringApplication.run(ProductApplication.class, args);
}
}
测试类:
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ProductApplicationTests {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@Test
void contextLoads() {}
@Test
void testGetAllProducts() {
List<Product> products = restTemplate.getForObject("http://localhost:" + port + "/api/products/", List.class);
assertThat(products).hasSize(5);
}
@Test
void testAddProduct() {
Product product = new Product("P006", "平板", 2000.0, 70);
restTemplate.postForEntity("http://localhost:" + port + "/api/products/", product, Void.class);
List<Product> products = restTemplate.getForObject("http://localhost:" + port + "/api/products/", List.class);
assertThat(products).hasSize(6);
}
@Test
void testUpdateProduct() {
Product product = new Product("P001", "手机", 1500.0, 120);
restTemplate.put("http://localhost:" + port + "/api/products/1", product);
List<Product> products = restTemplate.getForObject("http://localhost:" + port + "/api/products/", List.class);
assertThat(products.get(0).getPrice()).isEqualTo(1500.0);
}
@Test
void testDeleteProduct() {
restTemplate.delete("http://localhost:" + port + "/api/products/2");
List<Product> products = restTemplate.getForObject("http://localhost:" + port + "/api/products/", List.class);
assertThat(products).hasSize(4);
}
@Test
void testGetTopSellingProducts() {
List<Product> topSellingProducts = restTemplate.getForObject("http://localhost:" + port + "/api/products/top-selling?topN=3", List.class);
assertThat(topSellingProducts).hasSize(3);
assertThat(topSellingProducts.get(0).getProductId()).isEqualTo("P004");
assertThat(topSellingProducts.get(1).getProductId()).isEqualTo("P005");
assertThat(topSellingProducts.get(2).getProductId()).isEqualTo("P001");
}
}
输出结果:
结论:在实际开发中,Spring Boot RESTful API 的应用场景非常广泛,需要根据实际问题选择合适的 RESTful API 设计。
本章我们学习了 Spring Boot RESTful API 开发与测试,包括 RESTful API 的定义与特点、Spring Boot RESTful API 的开发、Spring Boot RESTful API 的测试、Spring Boot RESTful API 的认证与授权、Spring Boot RESTful API 的实际应用场景,学会了在实际开发中处理 RESTful API 问题。其中,RESTful API 的定义与特点、Spring Boot RESTful API 的开发、Spring Boot RESTful API 的测试、Spring Boot RESTful API 的认证与授权、Spring Boot RESTful API 的实际应用场景是本章的重点内容。从下一章开始,我们将学习 Spring Boot 的其他组件、微服务等内容。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online