引言
众所周知,在SpringSecurity的鉴权体系中,最常用的就是使用 @PreAuthorize
进行鉴权:
java
@Service
public class MyCustomerService {
@PreAuthorize("hasAuthority('permission:read')")
@PostAuthorize("returnObject.owner == authentication.name")
public Customer readCustomer(String id) { ... }
}
@PreAuthorize
注解本身使用SpEL表达式进行解析,其中hasAuthority
方式是位于#root
SpEL Context 中 rootObject 对象的方法 即SecurityExpressionRoot
继承自SecurityExpressionOperations
接口的方法
尽管SpEL表达式相当灵活,可以调用Bean,可以调用静态方法,但是对于常见的判断当前用户有没有指定权限的写法而言相对啰嗦: 通常来说,权限字符串应该是静态定声明在常量类中,但如果要在SpEL表达式中引用,就需要
java
@Service
public class MyCustomerService {
@PreAuthorize("hasAnyAuthority('" + PermissionCode.PROJECT + "')")
public Customer readCustomer(String id) { ... }
//或者
@PreAuthorize("hasAnyAuthority(T(com.xxx.constant.PermissionCode).PROJECT)")
public Customer readCustomer2(String id) { ... }
}
特别是,如果需要引用多个权限字符串,编写起来相当麻烦
java
@Service
public class MyCustomerService {
@PreAuthorize("hasAnyAuthority('" + PermissionCode.DATA_FILLING + "','" + PermissionCode.ASSEMBLY + "')")
public Customer readCustomer(String id) { ... }
}
@PreAuthorize("hasAnyAuthority(T(com.xxx.constant.PermissionCode).DATA_FILLING,T(com.xxx.constant.PermissionCode).ASSEMBLY)")
public Customer readCustomer2(String id) { ... }
}
可谓相当啰嗦且容易出错,而且这只是最基础的权限,如果包含租户/组织架构的权限判定,参数就会更加复杂,这就需要一个自定义的鉴权注解来替代@PreAuthorize
简化鉴权编码
自定义注解
java
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@PreAuthorize("@authorizeCheck.checkAuthority(#root)")
public @interface Authorize {
/**
* 权限编码集合
*/
String[] value();
/**
* 租户ID的SpEL表达式
*/
@Language("SpEL") String tenantId() default "";
/**
* 多个权限校验码之间是否只要有一个满足即可还是必须全部满足
*/
boolean anyMatch() default true;
}
其中最重要的就是@PreAuthorize("@authorizeCheck.checkAuthority(#root)")
,@PreAuthorize
本身是具有元注解功能的,但是其所提供的的功能不够强大,我们不能通过@PreAuthorize
在元注解模式下在被调用方法中提取到我们的自定义注解信息,我们需要拓展以达到AOP切面的效果,而不直接使用AOP的原因是,融入SpringSecurity的体系中可以帮助我们减少很多麻烦和处理工作
拓展 @PreAuthorize 的元注解能力
元注解@PreAuthorize("@authorizeCheck.checkAuthority(#root)")
中,@authorizeCheck.checkAuthority
是调用名为authorizeCheck
的Bean的checkAuthority
方法,方法参数为#root
对象。
但是默认的#root
对象MethodSecurityExpressionRoot
并不包含方法信息,target
字段也只是方法所在类实例,即上述示例中的 MyCustomerService 对象
java
package org.springframework.security.access.expression.method;
class MethodSecurityExpressionRoot extends SecurityExpressionRoot implements MethodSecurityExpressionOperations {
private Object filterObject;
private Object returnObject;
private Object target;
//...
}
package org.springframework.security.access.expression;
public abstract class SecurityExpressionRoot implements SecurityExpressionOperations {
private final Supplier<Authentication> authentication;
private AuthenticationTrustResolver trustResolver;
private RoleHierarchy roleHierarchy;
private Set<String> roles;
private String defaultRolePrefix = "ROLE_";
public final boolean permitAll = true;
public final boolean denyAll = false;
private PermissionEvaluator permissionEvaluator;
public final String read = "read";
public final String write = "write";
public final String create = "create";
public final String delete = "delete";
public final String admin = "administration";
//...
}
我们通过注册一个子类化的DefaultMethodSecurityExpressionHandler
的来替换rootObject,使得其包含当前被鉴权的方法信息
我们先创建一个RootObject类
java
@Getter
@Setter
public class RootObject extends SecurityExpressionRoot implements MethodSecurityExpressionOperations {
private Object filterObject;
private Object returnObject;
private Object target;
private MethodInvocation methodInvocation;//重点对象,很可惜默认的root并不传递该对象
public RootObject(Authentication authentication) {//SpringSecurity5写法
super(authentication);
}
public RootObject(Supplier<Authentication> authentication) {//SpringSecurity6写法
super(authentication);
}
public void setThis(Object target) {
this.target = target;
}
@Override
public Object getThis() {
return this.target;
}
}
然后是子类化DefaultMethodSecurityExpressionHandler
,如下是SpringSecurity6中的写法,SpringSecurity6中新增了Supplier<Authentication> authentication
参数形式的系列方法
java
/**
* <a href="https://docs.spring.io/spring-security/reference/servlet/authorization/method-security.html#subclass-defaultmethodsecurityexpressionhandler">春季安全/Servlet 应用程序/授权/方法安全性/子类化DefaultMethodSecurityExpressionHandler</a>
*/
public class AuthorizeExpressionHandler extends DefaultMethodSecurityExpressionHandler {
@Override
public EvaluationContext createEvaluationContext(Supplier<Authentication> authentication, MethodInvocation mi) {
// 替换为自定义 ROOT OBJECT
MethodBasedEvaluationContext context = new MethodBasedEvaluationContext(createSecurityExpressionRoot(authentication, mi)
, AopUtils.getMostSpecificMethod(mi.getMethod(), AopProxyUtils.ultimateTargetClass(Objects.requireNonNull(mi.getThis()))), mi.getArguments(), getParameterNameDiscoverer());
context.setBeanResolver(getBeanResolver());
return context;
}
@Override
protected MethodSecurityExpressionOperations createSecurityExpressionRoot(Authentication authentication, MethodInvocation invocation) {
return createSecurityExpressionRoot(() -> authentication, invocation);
}
//创建自定义 ROOT OBJECT
private MethodSecurityExpressionOperations createSecurityExpressionRoot(Supplier<Authentication> authentication,
MethodInvocation invocation) {
RootObject root = new RootObject(authentication);
root.setMethodInvocation(invocation);
root.setThis(invocation.getThis());
root.setPermissionEvaluator(getPermissionEvaluator());
root.setTrustResolver(getTrustResolver());
root.setRoleHierarchy(getRoleHierarchy());
root.setDefaultRolePrefix(getDefaultRolePrefix());
return root;
}
}
然后将该Handler注册为Bean即可
java
@Bean
public MethodSecurityExpressionHandler methodSecurityExpressionHandler(ApplicationContext context) {
AuthorizeExpressionHandler expressionHandler = new AuthorizeExpressionHandler();
expressionHandler.setApplicationContext(context);
return expressionHandler;
}
这样一来,@PreAuthorize("@authorizeCheck.checkAuthority(#root)")
中#root
对象就是携带方法信息的我们自定义的rootObject
自定义鉴权方法读取自定义注解
创建一个类用于执行校验逻辑的Service
java
@Service("authorizeCheck") // Bean名称要和@PreAuthorize("@authorizeCheck.checkAuthority(#root)")中一样
@RequiredArgsConstructor
public class AuthorizeCheckSerivce {
private final UserService userService;
/**
* 通过自定义注解{@link Authorize}判断权限<br>
*
* @param root SpEL 中的#root
* @return 是否有权限
*/
public boolean checkAuthority(RootObject root) {
MethodInvocation methodInvocation = root.getMethodInvocation();
if (methodInvocation == null) {
return true;
}
Authorize annotation;
if (methodInvocation.getMethod().isAnnotationPresent(Authorize.class)) {
annotation = methodInvocation.getMethod().getAnnotation(Authorize.class);
} else if (methodInvocation.getMethod().getDeclaringClass().isAnnotationPresent(Authorize.class)) {
annotation = methodInvocation.getMethod().getDeclaringClass().getAnnotation(Authorize.class);
} else {
return true;
}
String name = root.getAuthentication().getName();
UserEntity userEntity = userService.loadUserByUsername(name);//自己系统的用户对象
if (userEntity == null) {
return false;
}
List<String> authorities;
// 自定义注解中如果有一些自己系统的特别参数,就在这里处理
if (StringUtils.hasText(annotation.tenantId())) {
// 使用 SpEL 表达式工具读取当前调用方法参数中的租户ID
@Nullable Long tenantId = SpELUtil.getExpression(annotation.tenantId(), root.getMethodInvocation()).getValue(Long.class);
// 提取对应租户权限
authorities = userEntity.getAuthorities().stream()
.filter(e -> e instanceof TenantAuthority authority
&& authority.tenantId().equals(tenantId))
.map(Authority::getAuthority).distinct().toList();
} else {
authorities = userEntity.getAuthorities().stream().map(Authority::getAuthority).toList();
}
String[] needAuthorities = annotation.value();
if (annotation.anyMatch()) {
for (String needAuthority : needAuthorities) {
if (authorities.contains(needAuthority)) {
return true;
}
}
return false;
}
for (String needAuthority : needAuthorities) {
if (!authorities.contains(needAuthority)) {
return false;
}
}
return true;
}
}
使用自定义注解
这样我们可以使用灵活的方式使用自定义注解,而不是编写冗长的SpEL了
java
@Service
public class MyCustomerService {
@Authorize(PermissionCode.PROJECT)
public Customer readCustomer(String id) { ... }
@Authorize({PermissionCode.PROJECT, PermissionCode.DATA_FILLING})
public Customer readCustomer2(String id) { ... }
@Authorize(value = {PermissionCode.PROJECT,PermissionCode.DATA_FILLING}, anyMatch = false)
public Customer readCustomer3(String id) { ... }
@Authorize(value = PermissionCode.PROJECT,tenantId = "#p0")
public Customer readCustomer4(Long tenantId, String id) { ... }
}