赞
踩
该文章,用于记录Spring Cloud Gateway与Spring Security集成过程,以及集成过程中遇到的部分问题。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<version>2.2.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
/** * 认证成功后处理,此处偷懒,将用户信息,使用JSON格式字符串添加请求头。 * 后续会基于JWS生成Token。 */ @Bean public ServerAuthenticationSuccessHandler successHandler() { return (exchange, authentication) -> { UserDetails user = (UserDetails) authentication.getPrincipal(); Map<String, Object> tokenInfo = new HashMap<>(); tokenInfo.put("USER_NAME", user.getUsername()); tokenInfo.put("AUTHORITIES", user.getAuthorities()); ServerHttpResponse response = exchange.getExchange().getResponse(); exchange.getExchange().getRequest().mutate().header("X-AUTHENTICATION-TOKEN", JSONObject.toJSONString(tokenInfo)); ResponseEntity<Map<String, Object>> responseEntity = new ResponseEntity<>(tokenInfo, HttpStatus.OK); return response.writeWith(Mono.just(response.bufferFactory().wrap(JSON.toJSONBytes(responseEntity)))); }; } /** * 认证失败处理 */ @Bean public ServerAuthenticationFailureHandler failureHandler() { return (exchange, exception) -> { ServerHttpResponse response = exchange.getExchange().getResponse(); Map<String, Object> responseBody = new HashMap<>(2); responseBody.put("ERROR_CODE", "000000"); responseBody.put("ERROR_TYPE", exception.getClass().getName()); responseBody.put("ERROR_MESSAGE", exception.getMessage()); ResponseEntity<Map<String, Object>> responseEntity = new ResponseEntity<>(responseBody, HttpStatus.INTERNAL_SERVER_ERROR); response.setStatusCode(HttpStatus.FORBIDDEN); return response.writeWith(Mono.just(response.bufferFactory().wrap(JSON.toJSONBytes(responseEntity)))); }; } /** * 无权限处理配置 */ @Bean public ServerAccessDeniedHandler accessDeniedHandler() { return (exchange, accessDeniedException) -> { ServerHttpResponse response = exchange.getResponse(); Map<String, Object> responseBody = new HashMap<>(2); responseBody.put("ERROR_CODE", "000000"); responseBody.put("ERROR_MESSAGE", "请求未授权"); ResponseEntity<Map<String, Object>> responseEntity = new ResponseEntity<>(responseBody, HttpStatus.FORBIDDEN); response.setStatusCode(HttpStatus.FORBIDDEN); return response.writeWith(Mono.just(response.bufferFactory().wrap(JSON.toJSONBytes(responseEntity)))); }; } /** * 类似于Spring MVC模式下,AuthenticationManager */ @Bean public ReactiveAuthenticationManager authenticationManager(UserDetailsManager userDetailsManager, PasswordEncoder passwordEncoder) { return authentication -> { final String username = authentication.getName(); final String password = (String) authentication.getCredentials(); return Mono.just(userDetailsManager.loadUserByUsername(username)) .filter(user -> passwordEncoder.matches(password, user.getPassword())) .switchIfEmpty(Mono.defer(() -> Mono.error(new BadCredentialsException("Invalid Credentials")))) .map(user -> new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities())); }; } /** * 简易版UserDetailsManager实现类,此处仅用于模拟用户信息,真实情况,请使用数据库存储。 */ @Bean public UserDetailsManager userDetailsService(PasswordEncoder passwordEncoder) { UserDetailsManager manager = new InMemoryUserDetailsManager(); manager.createUser(new User("que", passwordEncoder.encode("123456"), Arrays.asList(new SimpleGrantedAuthority("ADMIN")))); return manager; } @Bean public PasswordEncoder passwordEncoder() { return PasswordEncoderFactories.createDelegatingPasswordEncoder(); } @Bean public ServerSecurityContextRepository contextRepository() { return new MemoryCacheSecurityContextRepository(5, TimeUnit.MINUTES); // return new WebSessionServerSecurityContextRepository(); } /** * Security核心配置信息 * 将上述配置的ServerAuthenticationSuccessHandler、ServerAuthenticationFailureHandler、ServerAccessDeniedHandler、 * ReactiveAuthenticationManager、ServerSecurityContextRepository配置进ServerHttpSecurity。 * 配置方式,与Spring MVC模式下的Security配置类似。 */ @Bean public SecurityWebFilterChain securityFilterChain(ServerHttpSecurity httpSecurity, ServerAuthenticationSuccessHandler accessHandler, ServerAuthenticationFailureHandler failureHandler, ServerAccessDeniedHandler accessDeniedHandler, ReactiveAuthenticationManager authenticationManager, ServerSecurityContextRepository securityContextRepository) { return httpSecurity.formLogin() .authenticationManager(authenticationManager) .authenticationSuccessHandler(accessHandler) // .securityContextRepository(securityContextRepository) .authenticationFailureHandler(failureHandler) .and().csrf().disable() .exceptionHandling().accessDeniedHandler(accessDeniedHandler) .and() // 此处用于存储认证后的Authentication。 // 默认使用WebSessionServerSecurityContextRepository。 // 该Repository为ReactiveSecurityContextHolder获取认证信息的数据来源。细节,后续部分介绍。 .securityContextRepository(securityContextRepository) // 配置自定义拦截器 .addFilterAt(authFilter, SecurityWebFiltersOrder.LOGIN_PAGE_GENERATING) .authorizeExchange(exchange -> { exchange.pathMatchers("/login").permitAll() .anyExchange().authenticated(); }) .build(); }
Web模式下(Spring Cloud Gateway 使用WebFlux),可通过SecurityContextHolder.getContext获取Authentication信息。此处无法使用该方式获取Authentication。原因在于Web模式下,若使用http.formLogin进行认证的话,请求通过UsernamePasswordAuthenticationFilter过滤器后,于successfulAuthentication(AbstractAuthenticationProcessingFilter类)存储认证信息。
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { if (logger.isDebugEnabled()) { logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult); } // 存储认证成功后的Authentication SecurityContextHolder.getContext().setAuthentication(authResult); rememberMeServices.loginSuccess(request, response, authResult); // Fire event if (this.eventPublisher != null) { eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent( authResult, this.getClass())); } successHandler.onAuthenticationSuccess(request, response, authResult); }
而WebFlux,使用WebFilter完成请求过滤,不会走Web模式下的Filter,认证信息,也就不会存储进SecurityContextHolder。
同样的,针对于WebFilter,Spring Security也提供ReactiveSecurityContextHolder存储Authentication,即也是通过过滤器,设置、获取Authentication。其底层,则是使用ServerSecurityContextRepository完成。
public class ReactorContextWebFilter implements WebFilter { private final ServerSecurityContextRepository repository; public ReactorContextWebFilter(ServerSecurityContextRepository repository) { Assert.notNull(repository, "repository cannot be null"); this.repository = repository; } @Override public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { return chain.filter(exchange) .subscriberContext(c -> c.hasKey(SecurityContext.class) ? c : withSecurityContext(c, exchange) ); } private Context withSecurityContext(Context mainContext, ServerWebExchange exchange) { return mainContext.putAll(this.repository.load(exchange) .as(ReactiveSecurityContextHolder::withSecurityContext)); } }
完成上述操作后,即完成Security的配置。接下来,实现一个请求,用于测试Security配置。此处,通过ReactiveSecurityContextHolder.getContext()获取登录用户信息,其底层,使用ServerSecurityContextRepository.load方法,获取Authentication。
@Slf4j @RestController @RequestMapping("quelongjiang/gatewayController") public class GatewayController { @GetMapping("info/{id}") public Mono<String> info(@PathVariable Integer id) throws InterruptedException { return ReactiveSecurityContextHolder.getContext() .filter(securityContext -> securityContext != null) .map(securityContext -> securityContext.getAuthentication()) .map(auth -> this.getAuthUserName(auth) + ", Request Argument is " + id); } // 获取登录用户名称 protected String getAuthUserName(Authentication auth) { if (!auth.isAuthenticated()) { return "Not Authentication"; } else { Object principal = auth.getPrincipal(); if (principal instanceof UserDetails) { return ((UserDetails) principal).getUsername(); } else { return String.valueOf(principal); } } } }
在securityFilterChain配置方法处,细心的读者会发现,有两行代码用于设置ServerSecurityContextRepository,其中第一行被注释掉。若把改行注释取消,同时将下面那行securityContextRepository(securityContextRepository)注释的话,会出现,需要认证的请求,永远会重定向到登录页面,即使已经完成认证。
该问题的原因,需通过ServerHttpSecurity看起。在ServerHttpSecurity类中,存在securityContextRepository三个方法。而当前需通过第一个方法设置,用于设置ServerHttpSecurity.securityContextRepository属性。该属性,为后续3个属性配置的默认值。
当该属性不为null时,则FormLoginSpec.securityContextRepository使用该属性,否则使用WebSessionServerSecurityContextRepository实现类,配置ReactorContextWebFilter。
当存在自定义ServerSecurityContextRepository实现类时,按照最初配置方式,其实配置进的是FormLoginSpec.securityContextRepository,这样会导致基于httpSecurity.formLogin,完成用户登录时,Authentication保存的是自定义的Repository,而ReactorContextWebFilter,则使用WebSessionServerSecurityContextRepository获取Authentication,导致获取不到Authentication,从而导致请求直接重定向到登录页面。
private WebFilter securityContextRepositoryWebFilter() {
ServerSecurityContextRepository repository = this.securityContextRepository == null ?
new WebSessionServerSecurityContextRepository() : this.securityContextRepository;
WebFilter result = new ReactorContextWebFilter(repository);
return new OrderedWebFilter(result, SecurityWebFiltersOrder.REACTOR_CONTEXT.getOrder());
}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。