关于Spring Security的方法鉴权

介绍

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

java 复制代码
@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等检查权限。比如:

java 复制代码
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

    less 复制代码
    @Configuration
    @EnableMethodSecurity
    public class SomeConfiguration {
        // ...
    }
  • 在Service或者Controller的方法上添加相应注解

    less 复制代码
    @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

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

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

自定义MethodSecurityExpressionHandler

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

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

指定自己的AuthorizationManager

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

先定一个自定义的AuthorizationManager类:

java 复制代码
@Component
public class MyAuthorizationManager implements AuthorizationManager<MethodInvocation> {

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

然后,在Configuration中指定它:

java 复制代码
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor preAuthorize(MyAuthorizationManager manager) {
    return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(manager);
}

原理分析

配置分析

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

java 复制代码
//...
@Import(MethodSecuritySelector.class)
public @interface EnableMethodSecurity {
    //...
}

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

java 复制代码
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来说,这个类的主要内容如下:

java 复制代码
@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,在这个类里面,可以看到如下方法:

java 复制代码
@Override
public Object invoke(MethodInvocation mi) throws Throwable {
    attemptAuthorization(mi);
    return mi.proceed();
}

它用来开启权限检查,而权限检查本身其实是通过调用AuthorizationManager#check方法来进行的。

接下来,我们再看PreAuthorizeAuthorizationManager,这个类是处理@PreAuthorize注解的授权管理器。它的主要内容如下:

java 复制代码
@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表达式)进行计算,得到一个布尔值,决定是否通过权限检查。

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

相关推荐
小李不想输啦10 分钟前
什么是微服务、微服务如何实现Eureka,网关是什么,nacos是什么
java·spring boot·微服务·eureka·架构
张铁铁是个小胖子11 分钟前
微服务学习
java·学习·微服务
ggs_and_ddu12 分钟前
Android--java实现手机亮度控制
android·java·智能手机
敲代码娶不了六花2 小时前
jsp | servlet | spring forEach读取不了对象List
java·spring·servlet·tomcat·list·jsp
Yhame.2 小时前
深入理解 Java 中的 ArrayList 和 List:泛型与动态数组
java·开发语言
是小崔啊3 小时前
开源轮子 - EasyExcel02(深入实践)
java·开源·excel
myNameGL4 小时前
linux安装idea
java·ide·intellij-idea
青春男大4 小时前
java栈--数据结构
java·开发语言·数据结构·学习·eclipse
HaiFan.4 小时前
SpringBoot 事务
java·数据库·spring boot·sql·mysql
我要学编程(ಥ_ಥ)4 小时前
一文详解“二叉树中的深搜“在算法中的应用
java·数据结构·算法·leetcode·深度优先