java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > SpringSecurity方法鉴权

Spring Security方法鉴权的实现

作者:龙鹤鹿

在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),原则上可以是任何类型。在实际的应用中,主要是以下几个:

基于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

这种方式,是完全无侵入的扩展,只需要向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方法鉴权内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文