通过Spring AOP结合SpEL表达式:构建强大且灵活的权限控制体系

开发 前端
通过本文的介绍,我们了解了如何使用Spring AOP和Spring Security的组合来实现权限验证。通过这种方式,我们可以提高应用程序的安全性,并降低代码的耦合度,提高代码的可重用性和可维护性。希望本文能够帮助读者更好地理解和应用Spring AOP和Spring Security,为他们的应用程序开发提供有益的参考。

环境:SpringBoot2.7.12

1.前言

在当今的Web应用程序中,权限验证是一个重要的安全措施,用于确保只有具有适当权限的用户才能访问特定的资源。随着应用程序的规模和复杂性的增加,实现权限验证变得更加困难。为了解决这个问题,我们可以使用Spring AOP(面向切面编程)和Spring Security的组合,它们可以提供一种有效的方法来实现权限验证。

在本文中,我们将探讨如何使用Spring AOP和Spring Security来实现权限验证。我们首先介绍Spring AOP和Spring Security的概念,然后解释如何将它们结合起来实现权限验证。通过这种方式,我们可以确保只有具有适当权限的用户能够访问受保护的资源,从而提高应用程序的安全性。

一、Spring AOP介绍


Spring AOP是Spring框架中的一个模块,用于支持面向切面编程。它允许开发者在应用程序中的关键点定义切面,从而对程序流程进行干预和控制。通过使用AOP,我们可以将与业务逻辑无关的代码(如日志记录、事务管理、权限认证等)抽取出来,并将其放在独立的切面中,这样可以提高代码的可重用性和可维护性。

二、Spring Security介绍


Spring Security是一个强大的安全框架,用于保护Web应用程序。它提供了丰富的安全特性,包括认证、授权、访问控制等。通过使用Spring Security,我们可以轻松地实现用户身份验证、角色授权、URL级别的访问控制等功能,从而确保只有经过授权的用户才能访问受保护的资源。

三、Spring AOP与Spring Security的组合


我们可以将Spring AOP与Spring Security结合起来实现权限验证。具体步骤如下:

  1. 定义一个Aspect切面,用于实现权限验证逻辑。该Aspect可以拦截用户对受保护资源的访问请求,并验证其权限。
  2. 定义一个Filter,该过滤器实现token的解析,将权限信息保存到当前的安全上下文中,最后添加到Security的过滤器链中。
  3. 在Aspect中,我们可以使用Spring Security提供的API来获取当前用户的身份信息、角色等信息,并根据业务需求判断用户是否具有访问受保护资源的权限。
  4. 如果用户没有足够的权限访问受保护资源,我们可以抛出一个异常,以阻止用户继续访问。
  5. 如果用户具有足够的权限访问受保护资源,我们可以允许用户继续访问该资源。

通过这种方式,我们可以轻松地实现权限验证,从而提高应用程序的安全性。同时,使用Spring AOP和Spring Security还可以降低代码的耦合度,提高代码的可重用性和可维护性。

2. 权限认证实现

相关依赖

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


<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>4.4.0</version>
</dependency>

权限认证过滤器

该过滤器的作用用来解析token,将权限信息添加到SecurityContext上下文中

public class PackAuthenticationFilter extends OncePerRequestFilter {


  public static final String TOKEN_NAME = "x-api-token" ;
  
  @SuppressWarnings("unused")
  private ApplicationContext context ;
  
  public PackAuthenticationFilter(ApplicationContext context) {
    this.context = context ;
  }
  
  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
      throws ServletException, IOException {
    String token = request.getHeader(TOKEN_NAME) ;
    if (!StringUtils.hasLength(token)) {
      response.setContentType("text/html;charset=UTF-8") ;
      response.getWriter().println("没有权限访问") ;
      return ;
    } 
    // 解析token
    List<? extends GrantedAuthority> authorities = JwtUtils.parseAuthority(token) ;
    Authentication authentication = new UsernamePasswordAuthenticationToken("", "", authorities) ;
    SecurityContextHolder.getContext().setAuthentication(authentication) ;
    filterChain.doFilter(request, response) ;
  }


}

安全配置类

将上面的过滤器添加到Security过滤器链中

@Configuration
public class SecurityConfig {
  
  @Autowired
  void setContext(ApplicationContext context) {
    this.context = context ;
  }
  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.csrf().disable();
    // 对所有的资源全部放行,我们只做对Controller接口的限制访问
    http.authorizeRequests().anyRequest().permitAll() ;
    // 添加过滤器
    http.addFilterBefore(new PackAuthenticationFilter(this.context), UsernamePasswordAuthenticationFilter.class) ;
    http.formLogin().disable() ;
    return http.build();
  }


}

自定义注解

该注解的作用用来标注具体的Controller接口。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface PreAuthority {
  
  String value() default "" ;
  
}

验证切面

该切面读取接口配置的权限,验证是否具有相应的权限

@Component
@Aspect
public class AuthenticationAspect {
  
  private AuthorityVerify authorityVerify ;
  
  public AuthenticationAspect(AuthorityVerify authorityVerify) {
    this.authorityVerify = authorityVerify ;
  }
  
  @Pointcut("@annotation(auth)")
  private void authority(PreAuthority auth) {}
  
  @Around("authority(auth)")
  public Object test(ProceedingJoinPoint pjp, PreAuthority auth) throws Throwable {
    String authority = auth.value() ;
    boolean permit = this.authorityVerify.hasAuthority(authority) ;
    if (!permit) {
      throw new RuntimeException("权限不足") ;
    }
    Object ret = pjp.proceed() ;
    return ret ;
  }
  
}

权限验证工具类

@Component
public class AuthorityVerify {


  public boolean hasAuthority(String authority) {
    Collection<? extends GrantedAuthority> authorities = SecurityContextHolder.getContext().getAuthentication().getAuthorities() ;
    return authorities.contains(new SimpleGrantedAuthority(authority)) ;
  }
  
}

全局异常处理

在上面的切面类中,如果没有权限是直接抛出的异常,所以这里定义一个全局异常对异常进行统一的处理。都比较简单,理解即可。

@RestControllerAdvice
public class GlobalExceptionAdvice {
  
  @ExceptionHandler({Exception.class})
  public Object exceptionProcess(Exception e) {
    return e.getMessage() ;
  }
}

测试接口

@RestController
@RequestMapping("/api")
public class ApiController {


  @GetMapping("/save")
  @PreAuthority("api:save")
  public Object save(HttpServletResponse response) throws Exception {
    return "save method invoke..." ;
  }
  
  @GetMapping("/{id}")
  @PreAuthority("api:query")
  public Object query(@PathVariable("id") Integer id) {
    return "query method invoke..." ;
  }
  
}

测试用户

Map<String, Object> map = new HashMap<>() ;
map.put("userId", "888888") ;
map.put("authorities", List.of("api:create", "api:query", "api:update", "api:delete")) ;
String token = createToken(map) ;
System.out.println(token) ;
String content = parseToken(token);
System.out.println(content) ;
System.out.println(">>>>>>>>>>>>>>>>>>>>>") ;
System.out.println(parseAuthority(token)) ;

这里模拟了一个用户信息,设置了权限集合,通过这些信息生成JWT信息。如下:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI4ODg4ODgiLCJhdXRob3JpdGllcyI6WyJhcGk6Y3JlYXRlIiwiYXBpOnF1ZXJ5IiwiYXBpOnVwZGF0ZSIsImFwaTpkZWxldGUiXSwiZXhwIjoxNjk5NjE3NTM3fQ.GGLYIP2g5RZZkBoLnyQ_NWOQq_NUQylr5iZH9ouDiCM

测试结果

图片图片

/api/save接口配置的权限是api:save,实际模拟的用户是没有这个权限的,所以这里看到的是切面中抛出的异常信息。

图片图片

查询接口正常访问。

以上是简单的示例,实际你应该会使用Spring Security结合数据库一起来验证管理用户的。

通过本文的介绍,我们了解了如何使用Spring AOP和Spring Security的组合来实现权限验证。通过这种方式,我们可以提高应用程序的安全性,并降低代码的耦合度,提高代码的可重用性和可维护性。希望本文能够帮助读者更好地理解和应用Spring AOP和Spring Security,为他们的应用程序开发提供有益的参考。

思考:

在上面的Controller中直接通过@PreAuthority('xxx')进行权限的设置,那我们是不是可以实现类似Spring Security提供@PreAuthorize("hasRole('xxx')")注解的功能,其中hasRole('xxx')是SpEL表达式。其实这里我们可以对切面稍加修改即可实现,部分代码如下:

初始化SpEL上下文:

@PostConstruct
public void init() {
  SpelParserConfiguration config = new SpelParserConfiguration(true, true);
  parser = new SpelExpressionParser(config) ;
  context = new StandardEvaluationContext() ;
  context.setRootObject(this.authorityVerify) ;
}

修改切面

@Around("authority(auth)")
public Object test(ProceedingJoinPoint pjp, PreAuthority auth) throws Throwable {
  String authority = auth.value() ;
  boolean permit = this.parser.parseExpression(authority).getValue(this.context, Boolean.class) ;
  if (!permit) {
    throw new RuntimeException("不具备对应角色") ;
  }
  Object ret = pjp.proceed() ;
  return ret ;
}

修改接口

@GetMapping("/save")
@PreAuthority("hasRole({'ADMIN', 'MGR'})")
public Object save(HttpServletResponse response) throws Exception {
  return "save method invoke..." ;
}

该接口只要具有ADMIN或者MGR角色的都可以访问。

责任编辑:武晓燕 来源: Spring全家桶实战案例源码
相关推荐

2009-12-14 09:57:04

Lambda表达式

2024-09-06 10:05:47

SpELSpring权限

2019-03-13 08:56:07

JavaSpEL表达式注入

2023-08-01 23:04:40

Spring编程AOP

2023-10-10 08:16:07

Spring依赖注入SpEL表达式

2011-06-01 13:31:29

Mercurial开放源码

2023-03-23 18:40:18

Lambda编程C++

2024-03-01 08:51:01

Django查询表达式查询语句

2024-03-13 14:40:35

SpringCron表达式

2014-01-05 17:41:09

PostgreSQL表达式

2024-03-25 13:46:12

C#Lambda编程

2009-08-31 16:39:32

ASP.NET表达式树

2024-10-10 14:43:54

LambdaSpring编程

2022-09-09 00:25:48

Python工具安全

2022-09-08 11:35:45

Python表达式函数

2018-09-27 15:25:08

正则表达式前端

2023-06-12 15:06:58

2022-07-21 07:05:13

粒子动画CSS

2019-11-22 09:40:40

SpringJava编程语言

2012-06-26 10:03:58

JavaJava 8lambda
点赞
收藏

51CTO技术栈公众号