Spring Security方法鉴权的实现

 更新时间:2023年12月18日 11:10:18   作者:龙鹤鹿  
在Spring Security中,主要有两种鉴权方式,一个是基于web请求的鉴权,一个是基于方法的鉴权,本文就来介绍一下Spring Security方法鉴权的实现,感兴趣的可以了解一下

介绍

在Spring Security中,主要有两种鉴权方式,一个是基于web请求的鉴权,一个是基于方法的鉴权。无论哪种鉴权,都最终会交由AuhtorizationManager执行权限检查。

@FunctionalInterface
public interface AuthorizationManager<T> {
    default void verify(Supplier<Authentication> authentication, T object) {
       AuthorizationDecision decision = check(authentication, object);
       if (decision != null && !decision.isGranted()) {
          throw new AccessDeniedException("Access Denied");
       }
    }

    @Nullable
    AuthorizationDecision check(Supplier<Authentication> authentication, T object);

}

从AuthorizationManager#check方法可以看出,如果要执行权限检查,那么必要的两个要素是Authentication和被保护的对象。

Authenticaion已经在登录过程中保存到了SecurityContext中,是拿来直接用的对象

被保护的对象(即:secureObject),原则上可以是任何类型。在实际的应用中,主要是以下几个:

  • HttpServletRequest,在对根据路径进行模式匹配时使用
  • RequestAuthorizationContext,在对根据表达式对Web请求执行权限检查时使用
  • MethodInvocation,在执行方法鉴权时使用

基于web请求的鉴权,可以通过配置SecurityFilterChain来根据请求的Path、Method等检查权限。比如:

http
    .authorizeHttpRequests(requests -> requests
        .dispatcherTypeMatchers(DispatcherType.ERROR).permitAll()
        .requestMatchers("/login/redirect").permitAll()
        .requestMatchers("/secured/foo").hasAuthority("P0")
        .anyRequest().authenticated());

对于方法鉴权,通常是通过annotation来进行的。

方法鉴权实战

常用的有四个注解:@PreAuthorize,@PostAuthorize,@PreFilter以及@PostFilter。他们的使用非常简单,如下:

标准方式

在配置类上添加注解:@EnableMethodSecurity

@Configuration
@EnableMethodSecurity
public class SomeConfiguration {
    // ...
}

在Service或者Controller的方法上添加相应注解

@GetMapping("/other")
@PreAuthorize("hasAuthority('P1')") // 拥有P1权限才可以方法该方法
public String other(HttpSession session) {
    return getUsername() + "其他资源: " + session.getId();
}

注:@PreAuthorize等注解的参数中,之所以能够使用一些内置对象和方法(比如:hasRole、returnObject,principal),是因为使用的上下文对象中,有一个root对象(MethodSecurityExpressionOperations),所有这些注解中使用的内置对象和方法都来自它。

扩展

有些时候,默认的方式不能满足业务需求,比如:从Authentication#getAuthorities得到的信息不足以满足业务需求,需要从数据库中查询数据。此时就需要扩展Spring Security的授权功能。
从扩展范围从小到大可以分为如下三种扩展方式:

  • 自定义Bean,然后提供权限检查方法
  • 自定义MethodSecurityExpressionHandler
  • 指定自己的AuthrozationManager实现

自定义Bean

这种方式,是完全无侵入的扩展,只需要向Spring容器注册一个Bean,给一个名字,然后接可以在@PreAuthorize等注解中使用这个bean的方法。

定义Bean

@Component("authz")
  public class CipherAuthorization {
      public boolean hasPerm(String permission) {
          // 从数据库中查询当前登录用户的所有权限
          // 查看permission是否在返回的权限集合之中,是则返回true,否则false
          boolean foundMatch = ...
          return foundMatch;
      }
  }

在业务类中使用

@Service
public class MyService {
    
    @PreAuthorize("@authz.hasPerm('system:edit')")
    public void updateData(...) {
        //...
    }
}

自定义MethodSecurityExpressionHandler

这种方式,可以修改解析@PreAuthorize表达式的方式。通常我们可以复用DefaultMethodSecurityExpressionHandler,或者实现一个它的子类。无论哪种方式,都是对这个Handler进行了定制。比如:

@Bean
static MethodSecurityExpressionHandler methodSecurityExpressionHandler() {
    DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
    // 定制handler,比如指定一个RoleHierarchy
    return handler;
}

指定自己的AuthorizationManager

这种方式,是彻底定制化了权限检查的整个过程,完全使用我们自己定义的AuthorizationManager实现类。比如:
先定一个自定义的AuthorizationManager类:

@Component
public class MyAuthorizationManager implements AuthorizationManager<MethodInvocation> {

    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation invocation) {
        // 执行自己的权限检查
    }
}

然后,在Configuration中指定它:

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor preAuthorize(MyAuthorizationManager manager) {
    return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(manager);
}

原理分析

配置分析

在方法鉴权中,使用了Spring AOP(当然,也可以指定AspectJ实现)来拦截被注解的方法。每个注解都对应一个Advisor。这一点,可以通过@EnableMethodSecurity这个注解查看。

//...
@Import(MethodSecuritySelector.class)
public @interface EnableMethodSecurity {
    //...
}

这里import了MethodSecuritySelector,它的主要内容如下:

if (annotation.prePostEnabled()) {
    imports.add(PrePostMethodSecurityConfiguration.class.getName());
}
if (annotation.securedEnabled()) {
    imports.add(SecuredMethodSecurityConfiguration.class.getName());
}
if (annotation.jsr250Enabled()) {
    imports.add(Jsr250MethodSecurityConfiguration.class.getName());
}

对于@PreAuthorize和PostAuthorize两个注解来说,使用到了同一个配置类:PrePostMethodSecurityConfiguration。

这里拿@PreAuthorize来说,这个类的主要内容如下:

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor preAuthorizeAuthorizationMethodInterceptor(...) {
    PreAuthorizeAuthorizationManager manager = new PreAuthorizeAuthorizationManager();
    manager.setExpressionHandler(
           expressionHandlerProvider.getIfAvailable(() -> defaultExpressionHandler(defaultsProvider, context)));
    AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
           .preAuthorize(manager(manager, registryProvider));
    strategyProvider.ifAvailable(preAuthorize::setSecurityContextHolderStrategy);
    eventPublisherProvider.ifAvailable(preAuthorize::setAuthorizationEventPublisher);
    return preAuthorize;
}

可以看到,处理@PreAuthorize注解的Advisor是AuthorizationManagerBeforeMethodInterceptor,而AuthorizationManager是PreAuthorizeAuthorizationManager。

运行分析

有了上述的配置,再来看运行。

首先看AuthorizationManagerBeforeMethodInterceptor,在这个类里面,可以看到如下方法:

@Override
public Object invoke(MethodInvocation mi) throws Throwable {
    attemptAuthorization(mi);
    return mi.proceed();
}

它用来开启权限检查,而权限检查本身其实是通过调用AuthorizationManager#check方法来进行的。
接下来,我们再看PreAuthorizeAuthorizationManager,这个类是处理@PreAuthorize注解的授权管理器。它的主要内容如下:

@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation mi) {
    ExpressionAttribute attribute = this.registry.getAttribute(mi);
    if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) {
       return null;
    }
    EvaluationContext ctx = this.registry.getExpressionHandler().createEvaluationContext(authentication, mi);
    boolean granted = ExpressionUtils.evaluateAsBoolean(attribute.getExpression(), ctx);
    return new ExpressionAuthorizationDecision(granted, attribute.getExpression());
}

可以看到,它首先从registry中找到MethodSecurityExpressionHandler,然后通过调用它的createEvaluationContext方法获取EvaluationContext,然后对@PreAuthorize的参数(SpEL表达式)进行计算,得到一个布尔值,决定是否通过权限检查。

到此这篇关于Spring Security方法鉴权的实现的文章就介绍到这了,更多相关SpringSecurity方法鉴权内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Feign实现多文件上传,Open Feign多文件上传问题及解决

    Feign实现多文件上传,Open Feign多文件上传问题及解决

    这篇文章主要介绍了Feign实现多文件上传,Open Feign多文件上传问题及解决,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-11-11
  • 详解maven配置多仓库的方法示例

    详解maven配置多仓库的方法示例

    这篇文章主要介绍了详解maven配置多仓库的方法示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-08-08
  • Intellij IDEA连接Navicat数据库的方法

    Intellij IDEA连接Navicat数据库的方法

    这篇文章主要介绍了Intellij IDEA连接Navicat数据库的方法,本文通过图文并茂的形式给大家介绍的非常详细,对大家的学习或工作具有一定的参考借价值,需要的朋友可以参考下
    2021-03-03
  • 简单介绍区分applet和application的方法

    简单介绍区分applet和application的方法

    applet和application都是Java语言编写出来的应用程序,本文简单介绍了二者的不同之处,需要的朋友可以参考下
    2017-09-09
  • springboot对接minio的webhook完整步骤记录

    springboot对接minio的webhook完整步骤记录

    Minio是一款开源的对象存储服务,它致力于为开发者提供简单、高性能、高可用的云存储解决方案,下面这篇文章主要给大家介绍了关于springboot对接minio的webhook的相关资料,需要的朋友可以参考下
    2024-07-07
  • java实现打印正三角的方法

    java实现打印正三角的方法

    这篇文章主要为大家详细介绍了java实现打印正三角的方法,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-01-01
  • SpringBoot实现TCP连接并进行数据互传的方法

    SpringBoot实现TCP连接并进行数据互传的方法

    本文详细介绍了微服务架构中的翻译组件使用场景,以及多种开源翻译组件的解决方案,文中分析了国内外多个翻译服务如百度翻译、谷歌翻译等,以及如何在微服务项目中集成这些翻译组件,感兴趣的朋友跟随小编一起看看吧
    2024-11-11
  • Springboot+rabbitmq实现延时队列的两种方式

    Springboot+rabbitmq实现延时队列的两种方式

    这篇文章主要介绍了Springboot+rabbitmq实现延时队列的两种方式,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-05-05
  • SpringBoot中的@RequestMapping注解的用法示例

    SpringBoot中的@RequestMapping注解的用法示例

    @RequestMapping注解是SpringBoot中最常用的注解之一,它可以帮助开发者定义和处理HTTP请求,本篇文章我们将详细为大家介绍如何使用SpringBoot中的@RequestMapping注解,感兴趣的同学跟着小编一起来学习吧
    2023-06-06
  • 使用Java实现查看线程的运行状态(附源码)

    使用Java实现查看线程的运行状态(附源码)

    在现代 Java 应用中,线程的运行状态对于排查问题和优化性能具有至关重要的作用,本文将使用Java编写一个查看线程运行状态的工具,有需要的可以了解下
    2025-03-03

最新评论