likes
comments
collection
share

关于Spring Security的方法鉴权

作者站长头像
站长
· 阅读数 9

介绍

在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等注解的参数中,之所以能够使用一些内置对象和方法(比如:hasRolereturnObjectprincipal),是因为使用的上下文对象中,有一个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());
}

对于@PreAuthorizePostAuthorize两个注解来说,使用到了同一个配置类: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,而AuthorizationManagerPreAuthorizeAuthorizationManager

运行分析

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

首先看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表达式)进行计算,得到一个布尔值,决定是否通过权限检查。

到此为止,整个过程就结束了。