天天看點

Spring Cloud Gateway擷取認證使用者資訊Spring Cloud Gateway擷取認證使用者資訊

Spring Cloud Gateway擷取認證使用者資訊

文章目錄

  • Spring Cloud Gateway擷取認證使用者資訊
    • 前言
    • 與Spring Security內建
      • 添加依賴
      • 配置類
    • 擷取認證使用者資訊
      • 擷取登入使用者
    • 頁面無限重定向登入頁面解決方法
    • 總結

前言

該文章,用于記錄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。

Spring Cloud Gateway擷取認證使用者資訊Spring Cloud Gateway擷取認證使用者資訊

當存在自定義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資訊,用于避免請求重定向到登入頁面的問題。

繼續閱讀