源码传送门:
https://github.com/ningzuoxin/zxning-springsecurity-demos/tree/master/02-springsecurity-stateless-webflux
一、前言
在上一篇,我们基于 Spring Webflux 集成 SpringSecurity 实现了前后端分离无状态 Rest API 的权限控制。那么,它与之前基于 Spring Web 集成 SpringSecurity 有何异同呢?我们先简单了解下
Spring Webflux 中所使用的一些过滤器的大致功能,再来分析这样配置实现的原理。
以下是我列举的一些 Spring Webflux 所使用的 SpringSecurity 相关的过滤器,和 Spring Web 中使用到的过滤器不太一样,Spring Webflux 中使用的过滤器都是实现了 WebFilter 接口。
org.springframework.security.config.web.server.ServerHttpSecurity.ServerWebExchangeReactorContextWebFilter
org.springframework.security.web.server.header.HttpHeaderWriterWebFilter
org.springframework.security.web.server.csrf.CsrfWebFilter
org.springframework.security.web.server.context.ReactorContextWebFilter
org.springframework.security.web.server.authentication.AuthenticationWebFilter
org.springframework.security.web.server.ui.LoginPageGeneratingWebFilter
org.springframework.security.web.server.ui.LogoutPageGeneratingWebFilter
org.springframework.security.web.server.context.SecurityContextServerWebExchangeWebFilter
org.springframework.security.web.server.savedrequest.ServerRequestCacheWebFilter
org.springframework.security.web.server.authentication.logout.LogoutWebFilter
org.springframework.security.web.server.authorization.ExceptionTranslationWebFilter
org.springframework.security.web.server.authorization.AuthorizationWebFilter
(1)CsrfWebFilter
CsrfWebFilter 的作用是为了防止 CSRF,在 LoginPageGeneratingWebFilter 创建默认的登录表单时,会往登录表单的隐藏文本写入一个 token,该 token 会随登录表单提交,在经过 CsrfWebFilter
时,会对其进行校验。对应源码如下:
// 校验 token
return this.validateToken(exchange);
(2)ReactorContextWebFilter
ReactorContextWebFilter 的作用是为了从 ServerSecurityContextRepository 加载 SecurityContext,ServerSecurityContextRepository 有两个默认的实现,分别是 WebSessionServerSecurityContextRepository(基于 WebSession 存取 SecurityContext)
和 NoOpServerSecurityContextRepository (返回空的 SecurityContext)。所以,在 Spring Webflux 的应用中要实现无状态的权限管理,我们需要自定义实现 ServerSecurityContextRepository。
private Context withSecurityContext(Context mainContext, ServerWebExchange exchange) {
// 从 ServerSecurityContextRepository 中加载 SecurityContext
return mainContext.putAll((Context)this.repository.load(exchange).as(ReactiveSecurityContextHolder::withSecurityContext));
}
(3)AuthenticationWebFilter
AuthenticationWebFilter 会处理 POST 方法提交的 /login 请求,将请求中的 username 和 password 转换为 UsernamePasswordAuthenticationToken 后,会交由 ReactiveAuthenticationManager 认证。
若认证成功,则执行 onAuthenticationSuccess 方法,创建 SecurityContext,将认证信息 Authentication 放入 SecurityContext,最终将 SecurityContext 保存在 ServerSecurityContextRepository中,
并调用 ServerAuthenticationSuccessHandler 的 onAuthenticationSuccess 方法。若认证失败,则执行 ServerAuthenticationFailureHandler 的 onAuthenticationFailure 方法。源码如下:
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
// 匹配 POST 方法提交的 /login 请求
return this.requiresAuthenticationMatcher.matches(exchange).filter((matchResult) -> {
return matchResult.isMatch();
}).flatMap((matchResult) -> {
// 转换成 UsernamePasswordAuthenticationToken
return this.authenticationConverter.convert(exchange);
}).switchIfEmpty(chain.filter(exchange).then(Mono.empty())).flatMap((token) -> {
// 执行认证
return this.authenticate(exchange, chain, token);
}).onErrorResume(AuthenticationException.class, (ex) -> {
// 认证失败,执行 ServerAuthenticationFailureHandler 的 onAuthenticationFailure 方法
return this.authenticationFailureHandler.onAuthenticationFailure(new WebFilterExchange(exchange, chain), ex);
});
}
// 认证成功,保存 SecurityContext,并执行 ServerAuthenticationSuccessHandler 的 onAuthenticationSuccess 方法
protected Mono<Void> onAuthenticationSuccess(Authentication authentication, WebFilterExchange webFilterExchange) {
ServerWebExchange exchange = webFilterExchange.getExchange();
SecurityContextImpl securityContext = new SecurityContextImpl();
securityContext.setAuthentication(authentication);
return this.securityContextRepository.save(exchange, securityContext).then(this.authenticationSuccessHandler.onAuthenticationSuccess(webFilterExchange, authentication)).subscriberContext(ReactiveSecurityContextHolder.withSecurityContext(Mono.just(securityContext)));
}
// 【注意】在 webflux 中是通过 getFormData 方法获取 username 和 password,所以请求的时候要指定格式为 x-www-form-urlencoded
public Mono<Authentication> apply(ServerWebExchange exchange) {
return exchange.getFormData().map((data) -> {
return this.createAuthentication(data);
});
}
(4)LoginPageGeneratingWebFilter
LoginPageGeneratingWebFilter 的作用是创建默认的登录表单。
(5)LogoutPageGeneratingWebFilterLogoutPageGeneratingWebFilter 的作用是创建默认的退出页面。
(6)LogoutWebFilterLogoutWebFilter 会处理 POST 方法提交的 /logout 请求,由 ServerLogoutHandler 处理退出逻辑,由 ServerLogoutSuccessHandler 处理退出成功逻辑。
public LogoutWebFilter() {
// 匹配的是 POST 方法提交的 /logout 请求
this.requiresLogout = ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, new String[]{"/logout"});
}
private Mono<Void> logout(WebFilterExchange webFilterExchange, Authentication authentication) {
logger.debug(LogMessage.format("Logging out user '%s' and transferring to logout destination", authentication));
// ServerLogoutHandler 处理退出,ServerLogoutSuccessHandler 处理退出成功
return this.logoutHandler.logout(webFilterExchange, authentication).then(this.logoutSuccessHandler.onLogoutSuccess(webFilterExchange, authentication)).subscriberContext(ReactiveSecurityContextHolder.clearContext());
}
(7)ExceptionTranslationWebFilter
ExceptionTranslationWebFilter 会处理 AccessDeniedException,若是匿名用户会交由 ServerAuthenticationEntryPoint 执行 commence 方法,默认是跳转登录页面。若是经过认证的用户,则会交由
ServerAccessDeniedHandler 处理,默认会响应 403。
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return chain.filter(exchange).onErrorResume(AccessDeniedException.class, (denied) -> {
return exchange.getPrincipal().filter((principal) -> {
return !(principal instanceof Authentication) || principal instanceof Authentication && !this.authenticationTrustResolver.isAnonymous((Authentication)principal);
}).switchIfEmpty(this.commenceAuthentication(exchange, new InsufficientAuthenticationException("Full authentication is required to access this resource"))).flatMap((principal) -> {
// 经过认证的用户,会交由 ServerAccessDeniedHandler 处理
return this.accessDeniedHandler.handle(exchange, denied);
}).then();
});
}
private <T> Mono<T> commenceAuthentication(ServerWebExchange exchange, AuthenticationException denied) {
// 匿名用户,默认会跳转到登录页面
return this.authenticationEntryPoint.commence(exchange, new AuthenticationCredentialsNotFoundException("Not Authenticated", denied)).then(Mono.empty());
}
(8)AuthorizationWebFilter
AuthorizationWebFilter 的作用是会对 SecurityContext 中的认证信息 Authentication 进行校验,校验会交由 DelegatingReactiveAuthorizationManager 处理。而校验的细节会分别交由
AuthenticatedReactiveAuthorizationManager(校验认证信息)和 AuthorityReactiveAuthorizationManager(校验权限集合),校验完毕会返回 AuthorizationDecision,若校验未通过在
ReactiveAuthorizationManager 返回 AccessDeniedException,并最终在 ExceptionTranslationWebFilter 处理。这里要注意一点,AuthorityReactiveAuthorizationManager 校验权限
只是校验配置在 SecurityWebFilterChain 中的权限控制,而在方法上,使用注解如 @PreAuthorize("hasAuthority('index')") 控制的权限,是通过 AOP 的形式实现的,后面我们会分析。
public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, T object) {
// AuthenticatedReactiveAuthorizationManager 主要是判断认证是否通过,是否是匿名用户
return authentication.filter(this::isNotAnonymous).map(this::getAuthorizationDecision).defaultIfEmpty(new AuthorizationDecision(false));
}
public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, T object) {
return authentication.filter(Authentication::isAuthenticated).flatMapIterable(Authentication::getAuthorities).map(GrantedAuthority::getAuthority).any((grantedAuthority) -> {
// AuthorityReactiveAuthorizationManager 主要功能是校验权限集合
return this.authorities.stream().anyMatch((authority) -> {
return authority.getAuthority().equals(grantedAuthority);
});
}).map((granted) -> {
return new AuthorityAuthorizationDecision(granted, this.authorities);
}).defaultIfEmpty(new AuthorityAuthorizationDecision(false, this.authorities));
}
default Mono<Void> verify(Mono<Authentication> authentication, T object) {
return this.check(authentication, object).filter(AuthorizationDecision::isGranted).switchIfEmpty(Mono.defer(() -> {
// 校验未通过就会在 ReactiveAuthorizationManager 封装成 AccessDeniedException
return Mono.error(new AccessDeniedException("Access Denied"));
})).flatMap((decision) -> {
return Mono.empty();
});
}
2、DefaultWebFilterChain 的重要作用
在 Spring Webflux 中,DefaultWebFilterChain 起着衔接 filter 和 handler 的重要作用。简单来说,过滤器链中还有过滤器未执行完毕时,就会执行 invokeFilter 方法,直到所有过滤器完全执行完毕,才会由 DispatcherHandler
查找到具体的 Handler 并执行 invokeHandler 方法。
public Mono<Void> filter(ServerWebExchange exchange) {
return Mono.defer(() -> {
return this.currentFilter != null && this.chain != null ? this.invokeFilter(this.currentFilter, this.chain, exchange) : this.handler.handle(exchange);
});
}
3、请求受保护资源,跳转登录流程分析
这一小节,可以参考下《【SpringSecurity系列2】基于SpringSecurity实现前后端分离无状态Rest API的权限控制原理分析》中的分析,流程差不多。
4、请求未授权资源,跳转异常处理流程分析这一小节,也可以参考下《【SpringSecurity系列2】基于SpringSecurity实现前后端分离无状态Rest API的权限控制原理分析》中的分析,大致流程也是差不多的,有几个地方我们分析下。
1、在 DefaultWebFilterChain 中,当所有过滤器执行完毕后,DispatcherHandler 会根据 HandlerMapping 找到具体的 RequestMappingHandlerAdapter 并执行 handle 方法。
2、在 RequestMappingHandlerAdapter 中,会根据 HandlerMethod 找到 InvocableHandlerMethod 并执行 invoke 方法。
3、在 InvocableHandlerMethod 中,执行 method.invoke(this.getBean(), args); 因为 this.getBean() 拿到的是某个 Controller 的代理对象,所以会进入 AOP 的切面逻辑,
也就是 CglibAopProxy 的内部类 DynamicAdvisedInterceptor 的 intercept 方法。
4、在 intercept 方法中,会执行到父类 ReflectiveMethodInvocation 的 proceed 方法,最终会在 PrePostAdviceReactiveMethodInterceptor 类的 invoke 方法中拿到权限注解
并开始进行权限的判断,PrePostAdviceReactiveMethodInterceptor 中相关源码如下:
// 拿到权限控制注解的属性,如:[authorize: 'hasRole('home')', filter: 'null', filterTarget: 'null']
PreInvocationAttribute preAttr = findPreInvocationAttribute(attributes);
// 从 SecurityContext 拿到 Authentication
Mono<Authentication> toInvoke = ReactiveSecurityContextHolder.getContext().map(SecurityContext::getAuthentication).defaultIfEmpty(this.anonymous).filter((auth) -> {
// 交由 PreInvocationAuthorizationAdvice 做权限的校验
return this.preInvocationAdvice.before(auth, invocation, preAttr);
}).switchIfEmpty(Mono.defer(() -> {
// 校验未通过就抛出 AccessDeniedException
return Mono.error(new AccessDeniedException("Denied"));
}));
5、最终抛出的 AccessDeniedException 会被 ExceptionTranslationWebFilter 处理。
三、配置的原理分析。在上一篇《【SpringSecurity系列1】基于SpringSecurity实现前后端分离无状态Rest API的权限控制》,我们修改了一些配置,从而使 SpringSecurity 满足了前后端分离架构的需要,那么我们这么配置的依据是什么呢?
1、为什么改写 GET 方法的 /login 请求?默认情况下 GET 方法的 /login 请求会跳转到 LoginPageGeneratingWebFilter 创建的默认登录表单。所以,我们需要改写 /login 请求,响应 JSON。
2、为什么需要 TokenServerAuthenticationSuccessHandler 和 TokenServerAuthenticationFailureHandler?登录请求经过 AuthenticationWebFilter 时,若认证成功会返回 Authentication,我们需要在 TokenServerAuthenticationSuccessHandler 的 onAuthenticationSuccess 方法,保存 Authentication 并返回 token,
若认证失败,则我们需要在 TokenServerAuthenticationFailureHandler 的 onAuthenticationFailure 方法响应认证失败提示。
默认情况下 LogoutWebFilter 在退出成功后,会由 RedirectServerLogoutSuccessHandler 将请求重定向到 LogoutPageGeneratingWebFilter 创建的退出页面。所以,我们需要创建 TokenServerLogoutSuccessHandler
在退出成功后,向客户端响应 JSON。
在请求无访问权限资源时,默认的 HttpStatusServerAccessDeniedHandler 会将请求响应状态码改为 403。所以,我们需要在 TokenServerAccessDeniedHandler 中向客户端响应 JSON。
5、为什么需要 TokenServerSecurityContextRepository?经过上文分析,ServerSecurityContextRepository 仅提供 WebSessionServerSecurityContextRepository(基于 WebSession 存取 SecurityContext) 和 NoOpServerSecurityContextRepository(返回空的 SecurityContext),
均不符合前后端分离无状态应用的业务需求。所以,我们需要自定义 TokenServerSecurityContextRepository,从而实现 SecurityContext 的存取。
为了防止 CSRF,默认 SpringSecurity 开启防 CSRF 配置,会在 CsrfWebFilter 中对登录表单进行 token 校验。
四、总结应该是我技术太菜,真心觉得在 Spring Webflux 中 DEBUG,有点反人类。哪位大神能分享下在 Spring Webflux 应用中 DEBUG 的经验不~
在下一篇,我们来看看 SpringSecurity OAuth2 该如何配置,大家多多关注哦~
【打个广告】推荐下个人的基于 SpringCloud 开源项目,供大家学习参考,欢迎大家留言进群交流
Gitee:https://gitee.com/ningzxspace/exam-ning-springcloud-v1
Github:https://github.com/ningzuoxin/exam-ning-springcloud-v1