Spring Boot Security Antmatchers入门教程
作为一个后端开发者,你所构建的API通常会被其他应用或服务所使用。一个应用所携带的数据的敏感性因应用不同而不同。
因此,在构建后端服务时,最理想的是只有经过认证的用户才能根据其授权级别访问这些数据。
主要收获
在这篇文章中。
- 我们将对保护你的应用程序的必要性有一个概述。
- 我们将进一步深入了解Springboot Antmatcher技术对应用程序的安全保护。
前提条件
- 对Springboot框架有基本的了解。
- 对应用程序编程接口(API)的基本了解。
- 一个合适的开发环境,如[IntelliJ]。
- [Postman API测试工具]或任何合适的浏览器。
保护你的应用程序的需要
当构建一个预计会传输敏感数据的应用程序时,你要确保这些数据不会落入坏人手中。
一个传输客户财务细节的应用程序应该被构建为适应最优质的安全协议和措施集。我们应该确保这样的服务足够安全,以防止数据泄露、网络攻击、数据损坏等。
一项服务所产生的数据的完整性取决于所采取的措施,以确保只有授权用户可以检索或操纵这些数据。
尽管如此,产生和传输的数据种类决定了你应该如何保护服务。
AntMatchers
antMatchers() 是一个Springboot的HTTP方法,用于配置Springboot应用安全应该根据用户的角色来允许请求的URL路径。antmatchers() 方法是一个重载方法,它接收HTTP请求方法和特定的URL作为它的参数。
Springboot使用antmatchers() ,通过将代表应用程序端点的模式与特定用户绑定来保护URL。然后,它根据用户的角色或权限,允许或拒绝对这些URL的访问。
以下是应用在antmatchers() 的一些方法。
-
hasAnyRole():这将URL绑定到任何用户,其角色包括在应用程序中创建的配置的角色中。它接收一个可变长度的角色参数。 -
hasRole():该方法接收一个与URL绑定的单一角色参数。 -
hasAuthority():该方法将URL与客户的授予权限绑定。任何被授予特定权限的客户端都被授权向URL发送请求。 -
hasAnyAuthority():该方法将URL绑定到任何用户,其授予的权限包括在应用程序中创建的配置的权限/许可。它接收一个可变长度的授予权限参数。 -
anonymous():将URL绑定到一个未认证的客户端。 -
authenticated():这将使URL与任何认证的客户端绑定。
让我们建立一个API来演示
让我们继续探索antMatchers 技术的特点。我们将建立一个由Product 模型和其资源组成的API。
项目结构
controller/
|--- ProductContoller.java
model/
|--- Product.java
repository/
|--- ProductRepository.java
security/
|--- AppSecurityConfig.java
|--- Roles.java
services/
|--- ProductService.java
|--- ProductServiceImpl.java
DemoApplication.java
角色
客户端的角色是以下其中之一。
- 实习生
- 监督员
- 管理员
服务器根据客户是否被授权从URL接收资源而授予客户的请求。
创建一个Enum类来声明用户的角色。
package com.example.demo.security;
public enum Roles {
INTERN,
SUPERVISOR,
ADMIN;
}
权限(Permissions
| 权限 | |||||
|---|---|---|---|---|---|
| 角色 | 添加一个产品 | 查看所有产品 | 查看产品 | 更新产品 | 删除产品 |
| 实习生 | 有 | 是的 | - | - | - |
| 监督员 | 是 | 有 | 是 | -` | - |
| 管理员 | 是 | 是的 | 是的 | 是 | 是的 |
该API所需的依赖性是。
- Spring Web
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
- Spring security
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- Guava maven
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.1-jre</version>
</dependency>
数据库
我将使用PostgreSQL数据库管理服务器来存储Product 模型的实例。你可以使用你选择的任何数据库。
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
在你的数据库管理服务器中创建一个Product 数据库。
数据库配置
在你的application.properties 文件中加入以下代码,为你的Springboot应用程序配置数据库。
spring.datasource.url=jdbc:postgresql://localhost:5432/product
spring.datasource.username=<username>
spring.datasource.password=<password>
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.PostgreSQLDialect
spring.jpa.properties.hibernate.format\_sql = true
server.error.include-message=always
用你的数据库服务器的实际用户名和密码替换<username> 和<password> 。
创建模型
@Entity
@Table
public class Product {
// Autogenerate the primary key
@Id
@SequenceGenerator(
name = "product\_sequence",
sequenceName = "product\_sequence",
allocationSize = 1
)
@GeneratedValue(
strategy = GenerationType.SEQUENCE,
generator = "product\_sequence"
)
private Integer productId;
private String name;
private String description;
private Double price;
public Product(Integer productId, String name, String desc, Double price){
this(name, desc, price);
this.productId = productId;
}
public Product(){
}
public Product(String name, String desc, Double price) {
this.name = name;
this.description = desc;
this.price = price;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Number getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
public Integer getProductId() {
return productId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
上面代码中的Product 模型有一个自动生成的主键--productId ,通过@SequenceGenerator 和GeneratedValue 注解生成。
存储库接口
这是应用程序中与数据库交互的部分。该接口扩展了JpaRepository 接口,这是一个Springboot内置的数据库交互接口。
@Repository
public interface ProductRepository extends JpaRepository<Product, Integer> {
}
服务接口
这个接口包含了对API将提供的服务的声明。并不总是必须声明这个接口。开发者可以跳过服务接口,继续进行实现。
然而,这种方法是可取的,因为它提高了你的应用程序的封装性。
@Service
public interface ProductService {
Product addProduct(Product product);
List<Product> getAllProducts();
Product getProduct(Integer productId);
void deleteProduct(Integer productId);
Product updateProduct(Integer productId, String productName, String productDescription, Double price);
}
服务实现
这包含了API的业务逻辑。在这里,我们从ProductService 接口中实现已经预定义的方法。
@Service
public class ProductServiceImpl implements ProductService{
@Autowired
private ProductRepository productRepository;
@Override
public Product addProduct(Product product) {
return productRepository.save(product);
}
@Override
public List<Product> getAllProducts() {
return productRepository.findAll();
}
@Override
public Product getProduct(Integer productId) {
return productRepository.findById(productId).orElseThrow(() -> new IllegalArgumentException("Invalid product id"));
}
@Override
public void deleteProduct(Integer productId) {
Product product = getProduct(productId);
productRepository.delete(product);
}
/* To update the value a property:
- validate that the new value is not null nor empty.
- validate that the new value is not the same as the old value to be replaced.
- If the values are the same, skip the operation.
*/
@Override
@Transactional
public Product updateProduct(Integer productId, String productName, String productDescription, Double price) {
Product product = getProduct(productId);
boolean emptyName = productName == null || productName.length() < 1;
boolean emptyProductDesc = productDescription == null || productDescription.length() < 1;
boolean validPrice = price != null && (price.compareTo((double) 0) > 0);
if (!emptyName && !product.getName().equals(productName)) {
product.setName(productName);
}
if (!emptyProductDesc && !product.getDescription().equals(productDescription)) {
product.setDescription(productDescription);
}
if(validPrice){
product.setPrice(price);
}
productRepository.save(product);
return product;
}
}
控制器
/*
All requests are received from the client and sent to the service for processing.
*/
@RestController
@RequestMapping("api/v1/products")
public class ProductController {
@Autowired
private ProductService productService;
@PostMapping("/add")
public Product addProduct(@RequestBody Product product){
return productService.addProduct(product);
}
// Get a product by its ID
@GetMapping("/{productId}")
public Product getProduct(@PathVariable("productId") Integer productId){
return productService.getProduct(productId);
}
@GetMapping
public List<Product> getAllProducts(){
return productService.getAllProducts();
}
@DeleteMapping("{productId}")
public void deleteProduct(@PathVariable("productId") Integer productId){
productService.deleteProduct(productId);
}
// The product ID is the only required argument.
@PutMapping(path = "/{productId}")
public Product updateProduct(
@PathVariable Integer productId,
@RequestParam(required =false) String productName,
@RequestParam(required =false) String productDesc,
@RequestParam(required =false) Double price
){
return productService.updateProduct(productId, productName, productDesc, price);
}
}
安全配置
最后,让我们创建一个类,在这里我们配置应用程序的安全操作。
在提供认证时,需要一个密码编码器来加密用户的密码。为了实现这一点,在你的主应用程序类或一个单独的类中添加下面的代码,并用@Configuration 注解来注释该类。
// Create and configure an instance of a Password encoder to encrypt the users' passwords.
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder(15);
}
HTTP会话被设置为无状态;因此,每次请求都需要认证。这将缓解在Postman上测试端点的演示。
如果客户端没有被授权请求该URL,无论客户端是否经过认证,请求都会以错误信息终止。
自定义用户的详细信息是通过UserDetailsService() 方法创建的,这是WebSecurityConfigurerAdapter 类的另一个重载方法。
这些细节如下
| 用户名 | 角色 |
|---|---|
| Timmy | 实习生 |
| john | 监督员 |
| 萨拉 | 管理员 |
客户的登录凭证是通过基本认证技术接收的。
// The @EnableWebSecurity annotation maps this class as a program which configures the security of the application.
@Configuration
@EnableWebSecurity
public class AppSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private final PasswordEncoder passwordEncoder;
public AppSecurityConfig(PasswordEncoder passwordEncoder) {
this.passwordEncoder = passwordEncoder;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
/*
Stateless session enables authentication for every request. This would help ease the demonstration
of this tutorial on Postman.
*/
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http
.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.DELETE, "/api/v1/products/{productId}").hasRole(ADMIN.name()) // Admin should be able to delete
.antMatchers(HttpMethod.PUT, "/api/v1/products/{productId}").hasRole(ADMIN.name()) // Admin should be able to update
.antMatchers("/api/v1/products/add").hasAnyRole(ADMIN.name(), SUPERVISOR.name()) // Admin and Supervisor should be able to add product.
.antMatchers("/api/v1/products").hasAnyRole(ADMIN.name(), SUPERVISOR.name(), INTERN.name()) // All three users should be able to get all products.
.antMatchers("/api/v1/products{productId}").hasAnyRole(ADMIN.name(), SUPERVISOR.name(), INTERN.name()) // All three users should be able to get a product by id.
.anyRequest()
.authenticated()
.and()
.httpBasic();
}
// Setting up the details of the application users with their respective usernames, roles and passwords.
@Bean
@Override
protected UserDetailsService userDetailsService() {
UserDetails timmy = User.builder()
.username("timmy")
.password(passwordEncoder.encode("password"))
.roles(INTERN.name())
.build();
UserDetails john = User.builder()
.username("john")
.password(passwordEncoder.encode("password"))
.roles(SUPERVISOR.name())
.build();
UserDetails sarah = User.builder()
.username("sarah")
.password(passwordEncoder.encode("password"))
.roles(ADMIN.name())
.build();
InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager(timmy, john, sarah);
return userDetailsManager;
}
}
邮递员
点击授权,将授权类型设置为Basic Auth。
通过Postman显示,我们通过antmatchers() 方法实现了基于客户端角色的授权。未经授权的请求被禁止,而授权的客户端收到了200的状态代码。
总结
在这篇文章中,我们了解了保护你的应用程序的必要性。我们探讨了一些保护应用程序的方法,并着手建立一个应用程序,展示AntMatchers如何被用来实现我们应用程序的安全水平。
网络攻击似乎不会在短期内永远消失。作为一个后端开发人员,你需要确保你的应用程序不容易受到攻击。