spring security 的webflux版本
由于Web容器不同,在Gateway项目中使用的WebFlux,是不能和Spring-Web混合使用的。
Spring MVC和 WebFlux 的区别:
Spring-Security配置:
spring security设置要采用响应式配置,基于WebFlux中WebFilter实现,与Spring MVC的Security是通过Servlet的Filter实现类似,也是一系列filter组成的过滤链。
Reactor与传统MVC配置对应:
webflux | mvc | 作用 |
---|---|---|
@EnableWebFluxSecurity | @EnableWebSecurity | 开启security配置 |
ServerAuthenticationSuccessHandler | AuthenticationSuccessHandler | 登录成功Handler |
ServerAuthenticationFailureHandler | AuthenticationFailureHandler | 登陆失败Handler |
ReactiveAuthorizationManager<AuthorizationContext> | AuthorizationManager | 认证管理 |
ServerSecurityContextRepository | SecurityContextHolder | 认证信息存储管理 |
ReactiveUserDetailsService | UserDetailsService | 用户登录 |
ReactiveAuthorizationManager | AccessDecisionManager | 鉴权管理 |
ServerAuthenticationEntryPoint | AuthenticationEntryPoint | 未认证Handler |
ServerAccessDeniedHandler | AccessDeniedHandler | 鉴权失败Handler |
引入的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
@EnableWebFluxSecurity
public class SecurityConfig {
@Resource(name = "authReactiveUserDetailService")
private AuthReactiveUserDetailService authReactiveUserDetailService;
@Resource(name = "customServerAuthenticationEntryPoint")
private ServerAuthenticationEntryPoint customServerAuthenticationEntryPoint;
@Resource(name = "customServerAccessDeniedHandler")
private ServerAccessDeniedHandler customServerAccessDeniedHandler;
@Resource(name = "refreshTokenWebFilter")
private RefreshTokenWebFilter refreshTokenWebFilter;
@Resource(name = "accessTokenWebFilter")
private AccessTokenWebFilter accessTokenWebFilter;
@Resource(name = "customServerSuccessHandler")
private CustomServerSuccessHandler customServerSuccessHandler;
@Resource(name = "customServerFailureHandler")
private CustomServerFailureHandler customServerFailureHandler;
@Value("${security.white-paths}")
private String[] whitePaths;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 如果没有,系统会自动创建一个
*/
@Bean
public ReactiveAuthenticationManager customAuthenticationManager() {
UserDetailsRepositoryReactiveAuthenticationManager manager
= new UserDetailsRepositoryReactiveAuthenticationManager(authReactiveUserDetailService);
manager.setPasswordEncoder(passwordEncoder());
return manager;
}
@Bean
ServerSecurityContextRepository customServerSecurityContextRepository(){
return NoOpServerSecurityContextRepository.getInstance();
}
@Bean
public SecurityWebFilterChain webFluxSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange()
//白名单不拦截
.pathMatchers(whitePaths).permitAll()
//AJAX进行跨域请求时的预检,需要向另外一个域名的资源发送一个HTTP OPTIONS请求头,用以判断实际发送的请求是否安全
.pathMatchers(HttpMethod.OPTIONS).permitAll().and()
.authorizeExchange()
.pathMatchers("/cs/apollo/**").hasAuthority("lll")
.anyExchange().authenticated()
.and()
.httpBasic(ServerHttpSecurity.HttpBasicSpec::disable)
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.authenticationManager(customAuthenticationManager())
.securityContextRepository(customServerSecurityContextRepository())
.cors()
.and()
.formLogin().loginPage("/orionLogin")
.authenticationSuccessHandler(customServerSuccessHandler)
.authenticationFailureHandler(customServerFailureHandler)
.and()
.logout().logoutUrl("/orionLogout").logoutSuccessHandler(new CustomServerLogoutHandler())
.and()
.exceptionHandling().accessDeniedHandler(customServerAccessDeniedHandler)
.authenticationEntryPoint(customServerAuthenticationEntryPoint);
http.addFilterAt(refreshTokenWebFilter, SecurityWebFiltersOrder.HTTP_BASIC);
http.addFilterAt(accessTokenWebFilter, SecurityWebFiltersOrder.AUTHORIZATION);
return http.build();
}
}
注解是@EnableWebFluxSecurity,其余跟spring security差不多,使用的语法不同,它都是由filter组成,上述使用了自定义的accessTokenFilter和refreshTokenFilter,添加的时候顺序需要注意一下,不同的filter都是由顺序的,如下图:
默认的顺序:
public enum SecurityWebFiltersOrder {
FIRST(Integer.MIN_VALUE),
HTTP_HEADERS_WRITER,
/**
* {@link org.springframework.security.web.server.transport.HttpsRedirectWebFilter}
*/
HTTPS_REDIRECT,
/**
* {@link org.springframework.web.cors.reactive.CorsWebFilter}
*/
CORS,
/**
* {@link org.springframework.security.web.server.csrf.CsrfWebFilter}
*/
CSRF,
/**
* {@link org.springframework.security.web.server.context.ReactorContextWebFilter}
*/
REACTOR_CONTEXT,
/**
* Instance of AuthenticationWebFilter
*/
HTTP_BASIC,
/**
* Instance of AuthenticationWebFilter
*/
FORM_LOGIN,
AUTHENTICATION,
/**
* Instance of AnonymousAuthenticationWebFilter
*/
ANONYMOUS_AUTHENTICATION,
OAUTH2_AUTHORIZATION_CODE,
LOGIN_PAGE_GENERATING,
LOGOUT_PAGE_GENERATING,
/**
* {@link org.springframework.security.web.server.context.SecurityContextServerWebExchangeWebFilter}
*/
SECURITY_CONTEXT_SERVER_WEB_EXCHANGE,
/**
* {@link org.springframework.security.web.server.savedrequest.ServerRequestCacheWebFilter}
*/
SERVER_REQUEST_CACHE,
LOGOUT,
EXCEPTION_TRANSLATION,
AUTHORIZATION,
LAST(Integer.MAX_VALUE);
private static final int INTERVAL = 100;
private final int order;
SecurityWebFiltersOrder() {
this.order = ordinal() * INTERVAL;
}
SecurityWebFiltersOrder(int order) {
this.order = order;
}
public int getOrder() {
return this.order;
}
}
第一次登陆的时候,会到 AuthenticationWebFilter,匹配上了requiresAuthenticationMatcher的路径(配置类中的loginPage设置)和方法(Post),就会执行后续authenticate,就是去ReactiveUserDetailsService验证身份了,如果不是loginPage的路径,经过这个filter时就不会
去authenticate了,最后会到AuthorizationWebFilter,这个需要注意:
ServerHttpSecurity -->Access类:(先说明配置类中配置成permitAll的路径)
public AuthorizeExchangeSpec permitAll() {
return access( (a, e) -> Mono.just(new AuthorizationDecision(true)));
}
public AuthorizeExchangeSpec access(ReactiveAuthorizationManager<AuthorizationContext> manager) {
AuthorizeExchangeSpec.this.managerBldr
.add(new ServerWebExchangeMatcherEntry<>(
AuthorizeExchangeSpec.this.matcher, manager));
AuthorizeExchangeSpec.this.matcher = null;
return AuthorizeExchangeSpec.this;
}
public class AuthorizationWebFilter implements WebFilter {
private ReactiveAuthorizationManager<? super ServerWebExchange> accessDecisionManager;
public AuthorizationWebFilter(ReactiveAuthorizationManager<? super ServerWebExchange> accessDecisionManager) {
this.accessDecisionManager = accessDecisionManager;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return ReactiveSecurityContextHolder.getContext()
.filter(c -> c.getAuthentication() != null)
.map(SecurityContext::getAuthentication)
.as(authentication -> this.accessDecisionManager.verify(authentication, exchange))
.switchIfEmpty(chain.filter(exchange));
}
}
public interface ReactiveAuthorizationManager<T> {
/**
* Determines if access is granted for a specific authentication and object.
*
* @param authentication the Authentication to check
* @param object the object to check
* @return an decision or empty Mono if no decision could be made.
*/
Mono<AuthorizationDecision> check(Mono<Authentication> authentication, T object);
/**
* Determines if access should be granted for a specific authentication and object
*
* @param authentication the Authentication to check
* @param object the object to check
* @return an empty Mono if authorization is granted or a Mono error if access is
* denied
*/
default Mono<Void> verify(Mono<Authentication> authentication, T object) {
return check(authentication, object)
.filter( d -> d.isGranted())
.switchIfEmpty(Mono.defer(() -> Mono.error(new AccessDeniedException("Access Denied"))))
.flatMap( d -> Mono.empty() );
}
}
可见:
- ①配置hasAuthority的,是AuthorityReactiveAuthorizationManager
- ②配置.anyExchange().authenticated(),就是 AuthenticatedReactiveAuthorizationManager
- ③其他正常的permitAll,是默认设置是true,尚未搞清楚。但是设置的是new AuthorizationDecision(true)
check的方法在 ReactiveAuthorizationManager的接口类中,①和②按照各自的check方法校验即可。③是AuthorizationDecision为true的,默认应该通过了,因为①②是否通过最后都有一个AuthorizationDecision(boolean).
由上述代码可见,如果发生验证不通过,会产生new AccessDeniedException("Access Denied"),但这个exception最后会给到它上一个filter:ExceptionTranslationWebFilter,由它的commenceAuthentication方法处理,而其中他的 this.authenticationEntryPoint,就是我们在sercurity中自定义配置的entryPoint。
private <T> Mono<T> commenceAuthentication(ServerWebExchange exchange, AccessDeniedException denied) {
return this.authenticationEntryPoint.commence(exchange, new AuthenticationCredentialsNotFoundException("Not Authenticated", denied))
.then(Mono.empty());
}