介绍
在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
等注解的参数中,之所以能够使用一些内置对象和方法(比如:hasRole
、returnObject
,principal
),是因为使用的上下文对象中,有一个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());
}
对于@PreAuthorize
和PostAuthorize
两个注解来说,使用到了同一个配置类: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
,而AuthorizationManager
是PreAuthorizeAuthorizationManager
。
运行分析
有了上述的配置,再来看运行。
首先看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表达式)进行计算,得到一个布尔值,决定是否通过权限检查。
到此为止,整个过程就结束了。