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方法鉴权内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Mybatis 入参类型方式全面详解

    Mybatis 入参类型方式全面详解

    这篇文章主要为大家介绍了Mybatis入参的类型方式全面示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-06-06
  • Java安全 ysoserial CommonsCollections3示例分析

    Java安全 ysoserial CommonsCollections3示例分析

    这篇文章主要为大家介绍了Java安全 ysoserial CommonsCollections3示例分析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-10-10
  • Netty分布式NioEventLoop任务队列执行源码分析

    Netty分布式NioEventLoop任务队列执行源码分析

    这篇文章主要为大家介绍了Netty分布式NioEventLoop任务队列执行源码分析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-03-03
  • JavaWeb中JavaMail创建邮件和发送邮件

    JavaWeb中JavaMail创建邮件和发送邮件

    这篇文章主要介绍了JavaWeb中JavaMail创建邮件和发送邮件,较为详细的分析了JavaMail发送邮件的用法,是非常实用的技巧,需要的朋友可以参考下
    2015-12-12
  • 解决因jdk版本引起的TypeNotPresentExceptionProxy异常

    解决因jdk版本引起的TypeNotPresentExceptionProxy异常

    这篇文章介绍了解决因jdk版本引起的TypeNotPresentExceptionProxy异常的方法,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-12-12
  • java中Cookie被禁用后Session追踪问题

    java中Cookie被禁用后Session追踪问题

    这篇文章主要介绍了Java中Cookie被禁用后Session追踪问题,非常不错,具有参考借鉴价值,需要的朋友可以参考下
    2017-03-03
  • Java通过正则表达式获取字符串中数字的方法示例

    Java通过正则表达式获取字符串中数字的方法示例

    最近工作中遇到了一个需求,需要利用java获取字符串中的数字,尝试几种方法后发现利用正则表达式实现最为方法,下面这篇文章就主要介绍了Java通过正则表达式获取字符串中数字的方法,文中给出了详细的示例代码,需要的朋友可以参考下。
    2017-03-03
  • java应用占用内存过高排查的解决方案

    java应用占用内存过高排查的解决方案

    这篇文章主要介绍了java应用占用内存过高排查的解决方案,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-03-03
  • 深入java内存查看与分析详解

    深入java内存查看与分析详解

    本篇文章是对java内存查看进行了详细的分析介绍,需要的朋友参考下
    2013-05-05
  • Spring Boot 把配置文件和日志文件放到jar外部

    Spring Boot 把配置文件和日志文件放到jar外部

    如果不想使用默认的application.properties,而想将属性文件放到jar包外面,怎么做呢?下面小编给大家带来了两种方法解决Spring Boot 把配置文件和日志文件放到jar外部问题,感兴趣的朋友一起看看吧
    2018-02-02

最新评论