Spring Boot中使用注解拦截器实现通用校验器和基于角色的权限注解
通过使用Spring Boot的注解和拦截器,我们可以优雅地实现通用校验器和灵活的权限控制。本文将以电商交易系统为案例,详细讲解如何在Spring Boot中实现支持角色入参的权限校验器,以及如何通过注解拦截器实现通用校验器,提供高拓展性和可维护性的解决方案。
1. 背景介绍
在电商交易系统中,不同的用户角色(如普通用户、商家、管理员)拥有不同的操作权限。例如:
- 普通用户:可以浏览商品、下单购买。
- 商家用户:可以上架商品、管理库存。
- 管理员:可以管理用户、审核商家、处理投诉。
为了确保系统的安全性和业务逻辑的正确性,我们需要对用户的操作进行校验和权限控制。传统的方法可能会在每个接口中添加重复的校验和权限判断代码,既不优雅也不利于维护。为了解决这个问题,我们可以使用Spring Boot的注解和拦截器机制,实现通用校验器和基于角色的权限控制。
2. Maven依赖
首先,我们需要在项目的pom.xml
中添加必要的依赖:
<dependencies>
<!-- Spring Boot Starter Web,用于构建Web应用 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Starter AOP,用于支持切面编程 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- 其他常用依赖,如数据库驱动 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
引入这些依赖后,我们可以使用Spring Boot的Web和AOP功能来实现我们的需求。
3. 项目结构设计
为了清晰地展示项目的实现,我们可以将项目结构设计如下:
src/main/java/com/example/ecommerce
|-- annotation // 自定义注解
| |-- ValidateUser.java // 通用校验注解
| |-- RequiresRoles.java // 支持角色入参的权限注解
|
|-- aspect // 拦截器和注解处理器
| |-- ValidationAspect.java // 校验拦截器
| |-- AuthorizationAspect.java // 权限拦截器
|
|-- controller // 控制器层
| |-- UserController.java
| |-- ProductController.java
| |-- OrderController.java
|
|-- service // 服务层
| |-- UserService.java
| |-- ProductService.java
| |-- OrderService.java
|
|-- model // 实体类
| |-- User.java
| |-- Product.java
| |-- Order.java
|
|-- util // 工具类
| |-- SessionUtil.java // 会话管理
|
|-- exception // 自定义异常
| |-- AuthorizationException.java
| |-- ValidationException.java
接下来,我们将详细介绍如何实现通用校验器和支持角色入参的权限注解。
4. 自定义注解实现通用校验器
4.1 定义通用校验注解
首先,我们需要定义一个通用的用户校验注解@ValidateUser
,用于校验用户的登录状态和信息完整性。
package com.example.ecommerce.annotation;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ValidateUser {
String message() default "用户校验失败";
}
该注解可以标记在需要校验用户的控制器方法上,通过AOP在方法执行前进行拦截和校验。
4.2 实现校验拦截器
接下来,实现ValidationAspect
类,对使用@ValidateUser
注解的方法进行拦截。
package com.example.ecommerce.aspect;
import com.example.ecommerce.annotation.ValidateUser;
import com.example.ecommerce.exception.ValidationException;
import com.example.ecommerce.model.User;
import com.example.ecommerce.util.SessionUtil;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class ValidationAspect {
@Pointcut("@annotation(com.example.ecommerce.annotation.ValidateUser)")
public void validateUserPointcut() {}
@Before("validateUserPointcut()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
User user = SessionUtil.getCurrentUser();
// 校验用户是否已登录
if (user == null) {
throw new ValidationException("用户未登录");
}
// 校验用户信息是否完整
if (user.getUsername() == null || user.getEmail() == null) {
throw new ValidationException("用户信息不完整");
}
}
}
在这里,我们通过SessionUtil.getCurrentUser()
获取当前用户(实际应用中可能从Session或Token中获取)。如果用户未登录或信息不完整,抛出自定义的ValidationException
异常。
5. 基于角色的权限注解实现
5.1 定义支持角色入参的权限注解
为了实现更加灵活的权限控制,我们需要定义一个支持角色入参的权限注解@RequiresRoles
。
package com.example.ecommerce.annotation;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequiresRoles {
/**
* 需要的角色列表
*/
String[] roles();
String message() default "无权限访问";
}
该注解可以接收一个角色数组roles
,表示只有满足这些角色之一的用户才能访问标记的方法。
5.2 实现权限拦截器
接下来,实现AuthorizationAspect
类,对使用@RequiresRoles
注解的方法进行拦截和权限校验。
package com.example.ecommerce.aspect;
import com.example.ecommerce.annotation.RequiresRoles;
import com.example.ecommerce.exception.AuthorizationException;
import com.example.ecommerce.model.User;
import com.example.ecommerce.util.SessionUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Arrays;
@Aspect
@Component
public class AuthorizationAspect {
@Pointcut("@annotation(com.example.ecommerce.annotation.RequiresRoles)")
public void requiresRolesPointcut() {}
@Around("requiresRolesPointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
User user = SessionUtil.getCurrentUser();
// 校验用户是否已登录
if (user == null) {
throw new AuthorizationException("用户未登录");
}
// 获取方法上的注解
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
RequiresRoles requiresRoles = method.getAnnotation(RequiresRoles.class);
// 获取需要的角色列表
String[] roles = requiresRoles.roles();
// 校验用户角色
if (Arrays.stream(roles).noneMatch(role -> role.equals(user.getRole()))) {
throw new AuthorizationException("无访问权限");
}
// 权限校验通过,执行方法
return joinPoint.proceed();
}
}
在这个拦截器中,我们:
- 获取当前用户,并校验是否已登录。
- 获取方法上的
@RequiresRoles
注解,提取需要的角色列表。 - 校验当前用户的角色是否在需要的角色列表中。
- 如果权限校验通过,执行目标方法;否则,抛出
AuthorizationException
异常。
6. 电商交易系统示例
接下来,我们将结合电商交易系统的实际场景,展示如何应用上述的通用校验器和基于角色的权限注解。
6.1 用户管理模块
首先,定义User
实体类和SessionUtil
工具类。
6.1.1 用户实体类
package com.example.ecommerce.model;
public class User {
private String username;
private String email;
private String role; // 用户角色,如 "user", "seller", "admin"
// 构造方法、Getter和Setter方法
}
6.1.2 会话管理工具类
package com.example.ecommerce.util;
import com.example.ecommerce.model.User;
public class SessionUtil {
// 模拟获取当前用户的方法
public static User getCurrentUser() {
// 实际应用中应从Session或Token中获取用户信息
return new User("张三", "zhangsan@example.com", "user");
}
}
6.2 商品管理模块
在商品管理模块中,不同的操作需要不同的权限。例如:
- 普通用户:可以查看商品列表。
- 商家用户:可以添加和修改商品。
- 管理员:可以删除任何商品。
6.2.1 商品实体类
package com.example.ecommerce.model;
public class Product {
private Long id;
private String name;
private Double price;
private String description;
// 构造方法、Getter和Setter方法
}
6.2.2 商品控制器
package com.example.ecommerce.controller;
import com.example.ecommerce.annotation.RequiresRoles;
import com.example.ecommerce.model.Product;
import com.example.ecommerce.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/product")
public class ProductController {
@Autowired
private ProductService productService;
// 所有用户都可以访问
@GetMapping("/list")
public List<Product> listProducts() {
return productService.getAllProducts();
}
// 商家和管理员可以添加商品
@RequiresRoles(roles = {"seller", "admin"})
@PostMapping("/add")
public String addProduct(@RequestBody Product product) {
productService.addProduct(product);
return "商品添加成功";
}
// 商家和管理员可以修改商品
@RequiresRoles(roles = {"seller", "admin"})
@PutMapping("/update")
public String updateProduct(@RequestBody Product product) {
productService.updateProduct(product);
return "商品更新成功";
}
// 只有管理员可以删除商品
@RequiresRoles(roles = {"admin"})
@DeleteMapping("/delete/{id}")
public String deleteProduct(@PathVariable Long id) {
productService.deleteProduct(id);
return "商品删除成功";
}
}
6.2.3 商品服务层
package com.example.ecommerce.service;
import com.example.ecommerce.model.Product;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ProductService {
// 模拟商品数据库
private List<Product> productList;
public List<Product> getAllProducts() {
// 返回所有商品
return productList;
}
public void addProduct(Product product) {
// 添加商品到数据库
productList.add(product);
}
public void updateProduct(Product product) {
// 更新商品信息
}
public void deleteProduct(Long id) {
// 从数据库删除商品
}
}
6.3 订单管理模块
在订单管理模块中,用户下单需要校验登录状态和用户信息完整性。
6.3.1 订单实体类
package com.example.ecommerce.model;
public class Order {
private Long id;
private Long productId;
private Integer quantity;
private String status;
// 构造方法、Getter和Setter方法
}
6.3.2 订单控制器
package com.example.ecommerce.controller;
import com.example.ecommerce.annotation.ValidateUser;
import com.example.ecommerce.model.Order;
import com.example.ecommerce.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private OrderService orderService;
// 提交订单,需要校验用户
@ValidateUser
@PostMapping("/submit")
public String submitOrder(@RequestBody Order order) {
orderService.submitOrder(order);
return "订单提交成功";
}
// 查询订单,需要登录
@ValidateUser
@GetMapping("/list")
public List<Order> listOrders() {
return orderService.getOrdersByUser();
}
}
6.3.3 订单服务层
package com.example.ecommerce.service;
import com.example.ecommerce.model.Order;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class OrderService {
// 模拟订单数据库
private List<Order> orderList;
public void submitOrder(Order order) {
// 提交订单逻辑
orderList.add(order);
}
public List<Order> getOrdersByUser() {
// 获取当前用户的订单列表
return orderList;
}
}
7. 拓展与总结
7.1 拓展思路
-
细化权限控制:可以进一步细化权限,例如增加
@RequiresPermissions
注解,基于具体的操作权限而非角色。public @interface RequiresPermissions { String[] permissions(); String message() default "无操作权限"; }
-
动态权限管理:将权限信息存储在数据库或配置中心,支持动态更新,避免硬编码角色和权限。
-
多重校验机制:结合参数校验、业务校验,构建更加完善的校验体系。
-
统一异常处理:使用
@ControllerAdvice
和@ExceptionHandler
统一处理校验和权限异常,提高代码的可维护性。@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(ValidationException.class) public ResponseEntity<String> handleValidationException(ValidationException ex) { return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ex.getMessage()); } @ExceptionHandler(AuthorizationException.class) public ResponseEntity<String> handleAuthorizationException(AuthorizationException ex) { return ResponseEntity.status(HttpStatus.FORBIDDEN).body(ex.getMessage()); } }
为了简化代码并结合@RequiresRoles
和@ValidateUser
的功能,我们可以创建一个新的注解@SecureAction
,同时支持用户校验和基于角色的权限控制。通过这个注解,我们可以在一个地方实现对用户的校验和角色权限的判断,避免多次注解的重复使用。
定义新的注解@SecureAction
@SecureAction
注解将结合@ValidateUser
和@RequiresRoles
的功能,校验用户的登录状态并判断其角色是否符合要求。我们可以通过注解的参数传递需要的角色列表。
package com.example.ecommerce.annotation;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SecureAction {
/**
* 需要的角色列表,如果为空则只校验用户登录状态
*/
String[] roles() default {};
String message() default "无权限或用户未登录";
}
在这个注解中,roles
参数可选,如果不传递角色则只校验用户的登录状态,传递角色时则会校验用户是否具有指定的角色。
实现拦截器
SecureActionAspect
拦截器将同时处理用户校验和角色校验。
package com.example.ecommerce.aspect;
import com.example.ecommerce.annotation.SecureAction;
import com.example.ecommerce.exception.AuthorizationException;
import com.example.ecommerce.exception.ValidationException;
import com.example.ecommerce.model.User;
import com.example.ecommerce.util.SessionUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Arrays;
@Aspect
@Component
public class SecureActionAspect {
@Pointcut("@annotation(com.example.ecommerce.annotation.SecureAction)")
public void secureActionPointcut() {}
@Around("secureActionPointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
User user = SessionUtil.getCurrentUser();
// 校验用户是否已登录
if (user == null) {
throw new ValidationException("用户未登录");
}
// 获取方法上的注解
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
SecureAction secureAction = method.getAnnotation(SecureAction.class);
// 获取需要的角色列表
String[] roles = secureAction.roles();
// 如果定义了角色,则进行角色校验
if (roles.length > 0 && Arrays.stream(roles).noneMatch(role -> role.equals(user.getRole()))) {
throw new AuthorizationException("无访问权限");
}
// 用户校验和角色校验通过,执行方法
return joinPoint.proceed();
}
}
示例:如何使用@SecureAction
注解
在控制器方法上使用@SecureAction
注解来同时实现用户登录状态和角色的校验:
package com.example.ecommerce.controller;
import com.example.ecommerce.annotation.SecureAction;
import com.example.ecommerce.model.Product;
import com.example.ecommerce.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/product")
public class ProductController {
@Autowired
private ProductService productService;
// 所有用户都可以查看商品
@GetMapping("/list")
public List<Product> listProducts() {
return productService.getAllProducts();
}
// 只有商家和管理员可以添加商品
@SecureAction(roles = {"seller", "admin"})
@PostMapping("/add")
public String addProduct(@RequestBody Product product) {
productService.addProduct(product);
return "商品添加成功";
}
// 只有管理员可以删除商品
@SecureAction(roles = {"admin"})
@DeleteMapping("/delete/{id}")
public String deleteProduct(@PathVariable Long id) {
productService.deleteProduct(id);
return "商品删除成功";
}
}
7.2 总结
通过本文的介绍,我们学习了如何在Spring Boot中使用自定义注解和拦截器,实现通用的用户校验器和支持角色入参的权限注解。这样的设计具有以下优点:
- 高可复用性:将校验和权限逻辑抽象为注解和拦截器,避免代码重复。
- 高可维护性:当需要修改校验或权限逻辑时,只需修改拦截器代码,无需逐个修改业务代码。
- 高拓展性:可以根据需求灵活添加新的校验规则或权限控制。
- 增强代码可读性:业务代码中通过注解直观地表达了需要的校验和权限要求。
在实际项目中,合理地使用注解和拦截器,可以大大提高开发效率和代码质量。希望本文的内容对您有所帮助,能够在项目实践中灵活运用。