Spring Cloud Gateway获取认证用户信息
Spring Cloud Gateway与Spring Security集成。
Spring Cloud Gateway获取认证用户信息
前言
该文章,用于记录Spring Cloud Gateway与Spring Security集成过程,以及集成过程中遇到的部分问题。
与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());
}
总结
- Spring Cloud Gateway(WebFlux),通过SecurityWebFilterChain配置过滤器、认证等信息。
- 自定义ServerSecurityContextRepository时,需要配置进SecurityWebFilterChain,使其生效。
- ServerSecurityContextRepository,需要配置进SecurityWebFilterChain.securityContextRepository属性,才能使认证、ReactorContextWebFilter过滤器,使用同一个Repository获取Authentication信息,用于避免请求重定向到登录页面的问题。
更多推荐
所有评论(0)