阅读本文之前,请投票支持这款 全新设计的脚手架 ,让 Java 再次伟大!
FilterSecurityInterceptor
FilterSecurityInterceptor 是负责权限验证的过滤器。一般来说,权限验证是一系列业务逻辑处理完成以后,最后需要解决的问题。所以默认情况下 security 会把和权限有关的过滤器,放在 VirtualFilter chains 的最后一位。
FilterSecurityInterceptor.doFilter 方法定义了两个权限验证逻辑。分别是 super.beforeInvocation 方法代表的权限前置验证与 super.afterInvocation 代表的后置权限验证。
java
public void invoke(FilterInvocation fi) throws IOException, ServletException {
if ((fi.getRequest() != null) && (fi.getRequest().getAttribute(FILTER_APPLIED) != null) && observeOncePerRequest) {
// filter already applied to this request and user wants us to observe
// once-per-request handling, so don't re-do security checking
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} else {
// first time this request being called, so perform security checking
if (fi.getRequest() != null && observeOncePerRequest) {
fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, null);
}
}
和身份验证的设计类似,前置权限验证的业务处理也是参考模板方法设计模式的理念,将核心处理流程定义在父类(super) AbstractSecurityInterceptor 中的。
java
InterceptorStatusToken token = super.beforeInvocation(fi);
再通过关联 AccessDecisionManager 的方式,把身份验证通过的认证对象委托给 AccessDecisionManager 执行。
java
Authentication authenticated = authenticateIfRequired();
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
.getAttributes(object);
// Attempt authorization
try {
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException accessDeniedException) {
publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
accessDeniedException));
throw accessDeniedException;
}
// Attempt to run as a different user
Authentication runAs = this.runAsManager.buildRunAs(authenticated, object,
attributes);
if (runAs == null) {
if (debug) {
logger.debug("RunAsManager did not change Authentication object");
}
// no further work post-invocation
return new InterceptorStatusToken(SecurityContextHolder.getContext(), false,
attributes, object);
}
else {
if (debug) {
logger.debug("Switching to RunAs Authentication: " + runAs);
}
SecurityContext origCtx = SecurityContextHolder.getContext();
SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
SecurityContextHolder.getContext().setAuthentication(runAs);
// need to revert to token.Authenticated post-invocation
return new InterceptorStatusToken(origCtx, true, attributes, object);
}
在前置权限校验执行完毕后,会使用 this.runAsManager.buildRunAs 与 SecurityContextHolder.getContext().setAuthentication(runAs) 方法将当前上下文中的身份认证成功的对象备份,然后重新拷贝一个新的鉴权成功的对象到上下文的中。
java
// Attempt to run as a different user
Authentication runAs = this.runAsManager.buildRunAs(authenticated, object,
attributes);
SecurityContext origCtx = SecurityContextHolder.getContext();
SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
SecurityContextHolder.getContext().setAuthentication(runAs);
这个处理初看会有点难以理解,但是仔细思考一下,这其实是一个友好且细腻的操作。
原因是 FilterSecurityInterceptor 在大部分情况下是最后一个默认过滤器,如果说还有后续过滤操作的话,那这些肯定是你的自定义的过滤器对象行为产生的。安全起见,security 将权限认证过后的 authenticated 对象拷贝了一份副本,方便用户在后续的自定义过滤器中使用。
即使自定义过滤器中不小心修改了全局上下文中的 authenticate 对象也无妨,因为 super.finallyInvocation 方法
java
finally {
super.finallyInvocation(token);
}
会保证自定义过滤器全部执行完毕以后,再把 copy source 「归还」到上下文中。这样,在后续的业务处理中就不用担心获取到一个不安全地 Authentication 对象了。
PrePostAnnotationSecurityMetadataSource.getAttributes
在 security 中使用的几种鉴权注解一共有以下四种:
- PreFilter
- PreAuthorize
- PostFilter
- PostAuthorize
其中最常用的是 PreAuthorize,它的含义是在执行目标方法前执行鉴权逻辑。前置过滤器执行的第一个逻辑,就是使用 getAttributes 方法获取目标方法上的注解。
java
public Collection<ConfigAttribute> getAttributes(Method method, Class<?> targetClass) {
if (method.getDeclaringClass() == Object.class) {
return Collections.emptyList();
}
logger.trace("Looking for Pre/Post annotations for method '" + method.getName()
+ "' on target class '" + targetClass + "'");
PreFilter preFilter = findAnnotation(method, targetClass, PreFilter.class);
PreAuthorize preAuthorize = findAnnotation(method, targetClass,
PreAuthorize.class);
PostFilter postFilter = findAnnotation(method, targetClass, PostFilter.class);
// TODO: Can we check for void methods and throw an exception here?
PostAuthorize postAuthorize = findAnnotation(method, targetClass,
PostAuthorize.class);
if (preFilter == null && preAuthorize == null && postFilter == null
&& postAuthorize == null) {
// There is no meta-data so return
logger.trace("No expression annotations found");
return Collections.emptyList();
}
String preFilterAttribute = preFilter == null ? null : preFilter.value();
String filterObject = preFilter == null ? null : preFilter.filterTarget();
String preAuthorizeAttribute = preAuthorize == null ? null : preAuthorize.value();
String postFilterAttribute = postFilter == null ? null : postFilter.value();
String postAuthorizeAttribute = postAuthorize == null ? null : postAuthorize
.value();
ArrayList<ConfigAttribute> attrs = new ArrayList<>(2);
PreInvocationAttribute pre = attributeFactory.createPreInvocationAttribute(
preFilterAttribute, filterObject, preAuthorizeAttribute);
if (pre != null) {
attrs.add(pre);
}
PostInvocationAttribute post = attributeFactory.createPostInvocationAttribute(
postFilterAttribute, postAuthorizeAttribute);
if (post != null) {
attrs.add(post);
}
attrs.trimToSize();
return attrs;
}
当通过 getAttributes 获取到对应的鉴权注解后,通过 createPreInvocationAttribute 将注解的 value------一般是 el 表达式,解析成 Express 对象后封装起来等待后续取用。
java
String preAuthorizeAttribute = preAuthorize == null ? null : preAuthorize.value();
PreInvocationAttribute pre = attributeFactory.createPreInvocationAttribute(
preFilterAttribute, filterObject, preAuthorizeAttribute);
java
public PreInvocationAttribute createPreInvocationAttribute(String preFilterAttribute,
String filterObject, String preAuthorizeAttribute) {
try {
// TODO: Optimization of permitAll
ExpressionParser parser = getParser();
Expression preAuthorizeExpression = preAuthorizeAttribute == null ? parser
.parseExpression("permitAll") : parser
.parseExpression(preAuthorizeAttribute);
Expression preFilterExpression = preFilterAttribute == null ? null : parser
.parseExpression(preFilterAttribute);
return new PreInvocationExpressionAttribute(preFilterExpression,
filterObject, preAuthorizeExpression);
}
catch (ParseException e) {
throw new IllegalArgumentException("Failed to parse expression '"
+ e.getExpressionString() + "'", e);
}
}
AccessDecisionManager.decide -> AffirmativeBased.decide
说完了 getAttribute,让我们回到 beforeInvocation 方法的后续处理。你可能已经猜到了,和身份认证一样 filter 是不会实现实际的权限验证逻辑的。权限验证被交给了过滤器关联的 AccessDecisionManage.decide 方法来决定。
AccessDecisionManager 是一个接口,它的实现一共有 3 个类。AbstractAccessDecisionManager 是实现接口的抽象类,提供模板方法的定义。AffirmativeBased、ConsensusBased、UnanimousBased 是三个继承 AbstractAccessDecisionManager 的子类实现,分别代表了三种不同的记分策略。
以 AffirmativeBased 作为代表来说明的话,主要内容就是把 authentication 与封装的鉴权注解 el 表达式传递给关联的成员 decisionVoters 的 vote 方法进行处理。
java
public void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
int deny = 0;
for (AccessDecisionVoter voter : getDecisionVoters()) {
int result = voter.vote(authentication, object, configAttributes);
if (logger.isDebugEnabled()) {
logger.debug("Voter: " + voter + ", returned: " + result);
}
switch (result) {
case AccessDecisionVoter.ACCESS_GRANTED:
return;
case AccessDecisionVoter.ACCESS_DENIED:
deny++;
break;
default:
break;
}
}
if (deny > 0) {
throw new AccessDeniedException(messages.getMessage(
"AbstractAccessDecisionManager.accessDenied", "Access is denied"));
}
// To get this far, every AccessDecisionVoter abstained
checkAllowIfAllAbstainDecisions();
}
PreInvocationAuthorizationAdviceVoter.vote
和上面的架构设计一样, AccessDecisionVoter 的实现 PreInvocationAuthorizationAdviceVoter.vote 看起来像是要开始处理真正意义上的权限问题了,但是其实依然没到最核心的部分。
vote 方法会返回一个 preAdvice.before 的结果,这个结果会被转换成 ACCESS_GRANTED : ACCESS_DENIED 这两个数字。为什么 preAdvice.before 的调用不直接返回 boolean 类型的鉴权结果?想想之前的 AffirmativeBased.decide 方法的设计思路:根据多个不同的 voter 返回的结果,我们可以使用不同的投票策略来对分数进行判定。返回一个数字,而不是 boolean 更便于计算最终得分。
java
public int vote(Authentication authentication, MethodInvocation method,
Collection<ConfigAttribute> attributes) {
// Find prefilter and preauth (or combined) attributes
// if both null, abstain
// else call advice with them
PreInvocationAttribute preAttr = findPreInvocationAttribute(attributes);
if (preAttr == null) {
// No expression based metadata, so abstain
return ACCESS_ABSTAIN;
}
boolean allowed = preAdvice.before(authentication, method, preAttr);
return allowed ? ACCESS_GRANTED : ACCESS_DENIED;
}
ExpressionBasedPreInvocationAdvice.before
这次我向你保证,ExpressionBasedPreInvocationAdvice.before 真的是专门负责判定权限校验是否通过的方法了。由于之前已经封装好了 el 表达式的 Express 对象,所以调用 ExpressionUtils.evaluateAsBoolean 就可以直接根据表达式与执行上下文 ctx -> WebSecurityExpressionRoot extends SecurityExpressionRoot 使用 getValue() 方法获取执行结果------即权限校验的结果。
java
public boolean before(Authentication authentication, MethodInvocation mi,
PreInvocationAttribute attr) {
PreInvocationExpressionAttribute preAttr = (PreInvocationExpressionAttribute) attr;
EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication,
mi);
Expression preFilter = preAttr.getFilterExpression();
Expression preAuthorize = preAttr.getAuthorizeExpression();
if (preFilter != null) {
Object filterTarget = findFilterTarget(preAttr.getFilterTarget(), ctx, mi);
expressionHandler.filter(filterTarget, preFilter, ctx);
}
if (preAuthorize == null) {
return true;
}
return ExpressionUtils.evaluateAsBoolean(preAuthorize, ctx);
}
举一反三一下,既然鉴权逻辑就是一个运行时的 el 表达式解析后的结果,那利用 el 表达式可以调用类方法的特性,是不是自定义一个专门的表达式处理程序就可以随心所欲地返回鉴权结果了呢?
自定义 EL 表达式
java
/**
* sysdomain + edit permission
*/
public static final String SYSDOMAIN_AND_EDITPERMISSION = SYSDOMAIN + " && " + EDIT_PERMISSION;
public static final String EDIT_PERMISSION_MENU = "@authorizeAppService.hasPermission('EDIT_PERMISSION_MENU')";
自定义 EL 表达式处理方法
java
public class AuthorizeAppService implements AuthorizeAppInterface {
@Override
public boolean hasDomain(String domain) throws IllegalArgumentException {
if (StringUtils.isEmpty(domain)) {
throw new IllegalArgumentException("hasDomain accepted a invalid express parameter");
}
AuthUser user = (AuthUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return StringUtils.equals(domain, user.getDomain().getCode());
}
@Override
public boolean hasRole(String... role) throws IllegalArgumentException {
if (ArrayUtils.isEmpty(role)) {
throw new IllegalArgumentException("hasRole accepted a invalid express parameter\"");
}
AuthUser user = (AuthUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return user.getAuthRoles().stream().map(AuthRole::getCode).collect(Collectors.toList()).containsAll(CollectionUtils.arrayToList(role));
}
@Override
public boolean hasPermission(String... permission) throws IllegalArgumentException {
if (ArrayUtils.isEmpty(permission)) {
throw new IllegalArgumentException("hasPermission accepted a invalid express parameter\"");
}
AuthUser user = (AuthUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
Set<String> permissions = user.getAllpermissions().stream().map(AuthPermission::getCode).collect(Collectors.toSet());
return permissions.containsAll(CollectionUtils.arrayToList(permission));
}
}
最后,我们来探讨一下 security 为什么要费尽周折,通过一长串的聚合和依赖把权限校验这个处理流程传递这么长的路径来实现?为什么不在一开始就是 make el Express and return el.value() 搞定所有的事情?
我想这是因为 security 是一个 architect first 的设计产物。架构优先的设计,高扩展性都特别的重要。这一系列的聚合和依赖,都是「用组合解决问题」思想的体现。尽量用组合来解决问题会带来了很多好处,比如通过 GlobalMethodSecurityConfiguration 类进行的高度可配置化。
java
@Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class GlobalMethodSecurityConfiguration
implements ImportAware, SmartInitializingSingleton, BeanFactoryAware
{
@Bean
public MethodSecurityMetadataSource methodSecurityMetadataSource() {
List<MethodSecurityMetadataSource> sources = new ArrayList<>();
ExpressionBasedAnnotationAttributeFactory attributeFactory = new ExpressionBasedAnnotationAttributeFactory(
getExpressionHandler());
MethodSecurityMetadataSource customMethodSecurityMetadataSource = customMethodSecurityMetadataSource();
if (customMethodSecurityMetadataSource != null) {
sources.add(customMethodSecurityMetadataSource);
}
boolean hasCustom = customMethodSecurityMetadataSource != null;
boolean isPrePostEnabled = prePostEnabled();
boolean isSecuredEnabled = securedEnabled();
boolean isJsr250Enabled = jsr250Enabled();
if (!isPrePostEnabled && !isSecuredEnabled && !isJsr250Enabled && !hasCustom) {
throw new IllegalStateException("In the composition of all global method configuration, " +
"no annotation support was actually activated");
}
if (isPrePostEnabled) {
sources.add(new PrePostAnnotationSecurityMetadataSource(attributeFactory));
}
if (isSecuredEnabled) {
sources.add(new SecuredAnnotationSecurityMetadataSource());
}
if (isJsr250Enabled) {
GrantedAuthorityDefaults grantedAuthorityDefaults =
getSingleBeanOrNull(GrantedAuthorityDefaults.class);
Jsr250MethodSecurityMetadataSource jsr250MethodSecurityMetadataSource = this.context.getBean(Jsr250MethodSecurityMetadataSource.class);
if (grantedAuthorityDefaults != null) {
jsr250MethodSecurityMetadataSource.setDefaultRolePrefix(
grantedAuthorityDefaults.getRolePrefix());
}
sources.add(jsr250MethodSecurityMetadataSource);
}
return new DelegatingMethodSecurityMetadataSource(sources);
}
}
异常处理
voter 与 advicer 只做他们的份内事------对鉴权结果投票并返回。不同的得分策略,决定了同样的分数在不同的策略下,可能会有不同的最终结果。
所以 AffirmativeBased.decide 如果得出了鉴权失败的结论的话就会抛出一个 AccessDeniedException 异常。这个异常会一直沿着调用链往上抛,直到抛到 FilterSecurityIntercptor 的调用者,ExceptionTranslationFilter 中。
java
if (deny > 0) {
throw new AccessDeniedException(messages.getMessage(
"AbstractAccessDecisionManager.accessDenied", "Access is denied"));
}
ExceptionTranslationFilter 过滤器专门处理和异常相关的事情。它的 doFilter 中定义的 AccessDeniedException 捕获处理会将异常处理交由 accessDeniedHandler.handle 处理。同时得益于组合的设计思想,accessDeniedHandler.handle 也是可配置化的。
回忆之前的自定义身份验证异常处理类 GlobalExceptionHandler。现在只要增加一个新的接口实现并重写 handle 方法,就可以在一个类里面处理身份验证和鉴权异常了。
java
private void handleSpringSecurityException(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, RuntimeException exception)
throws IOException, ServletException {
if (exception instanceof AuthenticationException) {
logger.debug(
"Authentication exception occurred; redirecting to authentication entry point",
exception);
sendStartAuthentication(request, response, chain,
(AuthenticationException) exception);
}
else if (exception instanceof AccessDeniedException) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {
logger.debug(
"Access is denied (user is " + (authenticationTrustResolver.isAnonymous(authentication) ? "anonymous" : "not fully authenticated") + "); redirecting to authentication entry point",
exception);
sendStartAuthentication(
request,
response,
chain,
new InsufficientAuthenticationException(
messages.getMessage(
"ExceptionTranslationFilter.insufficientAuthentication",
"Full authentication is required to access this resource")));
}
else {
logger.debug(
"Access is denied (user is not anonymous); delegating to AccessDeniedHandler",
exception);
accessDeniedHandler.handle(request, response,
(AccessDeniedException) exception);
}
}
}
java
@RestControllerAdvice
public class GlobalExceptionHandler implements AuthenticationEntryPoint, AccessDeniedHandler {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException {
log.error("spring security 认证发生异常。", e);
HttpResponseWriter.sendError(httpServletResponse, HttpServletResponse.SC_UNAUTHORIZED, "身份认证失败");
}
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException {
log.error("spring security 鉴权发生异常。", e);
HttpResponseWriter.sendError(httpServletResponse, HttpServletResponse.SC_FORBIDDEN, "鉴权失败");
}
}