Spring-Gateway與Spring-Security在前后端分離項目中的實踐


前言

網上貌似webflux這一套的SpringSecurity操作資料貌似很少。

自己研究了一波,記錄下來做一點備忘,如果能幫到也在迷惑的人一點點,就更好了。

新項目是前后端分離的項目,前台vue,后端SpringCloud2.0,采用oauth2.0機制來獲得用戶,權限框架用的gateway。

一,前台登錄

大概思路前台主要是配合項目中配置的clientId,clientSecret去第三方服務器拿授權碼code,然后拿這個code去后端交互,后端根據code去第三方拿用戶信息,由於第三方只保存用戶信息,不管具體的業務權限,所以我會在本地保存一份用戶副本,用來做權限關聯。用戶登錄成功后,會把一些用戶基本信息(脫敏)生成jwt返回給前端放到head中當Authorization,同時后端把一些相關聯的菜單,權限等數據放到redis里做關聯,為后面的權限控制做准備。

二,SpringSecurity的webflux應用

如果用過SpringSecurity,HttpSecurity應該是比較熟悉的,基於Web允許為特定的http請求配置安全性。

WebFlux中ServerHttpSecurity與HttpSecurity提供的相似的類似,但僅適用於WebFlux。默認情況下,它將應用於所有請求,但可以使用securityMatcher(ServerWebExchangeMatcher)或其他類似方法進行限制。

項目比較特殊,就不能全展示了,大概寫一寫,開啟Security如下:

@EnableWebFluxSecurity
public class MyExplicitSecurityConfiguration {
  @Bean
  SecurityWebFilterChain webFluxSecurityFilterChain(ServerHttpSecurity http) throws Exception {
    http.securityContextRepository(new NoOpServerSecurityContextAutoRepository(tokenProvider)).httpBasic().disable()
      .formLogin().disable()
      .csrf().disable()
      .logout().disable();
    http.addFilterAt(corsFilter(), SecurityWebFiltersOrder.CORS)
      .authorizeExchange()
      .matchers(EndpointRequest.to("health", "info"))
      .permitAll()
      .and()
      .authorizeExchange()
      .pathMatchers(HttpMethod.OPTIONS)
      .permitAll()
      .and()
      .authorizeExchange()
      .pathMatchers(HttpMethod.PUT)
      .denyAll()
      .and()
      .authorizeExchange()
      .pathMatchers(HttpMethod.DELETE)
      .denyAll()
      .and()
      .authorizeExchange()
      .pathMatchers(HttpMethod.HEAD)
      .denyAll()
      .and()
      .authorizeExchange()
      .pathMatchers(HttpMethod.PATCH)
      .denyAll()
      .and()
      .authorizeExchange()
      .pathMatchers(HttpMethod.TRACE)
      .denyAll()
      .and()
      .authorizeExchange()
      .pathMatchers(excludedAuthPages).permitAll()
      .and()
      .authorizeExchange()
      .pathMatchers(authenticatedPages).authenticated()
      .and()
      .exceptionHandling()
      .accessDeniedHandler(new AccessDeniedEntryPointd())
      .and()
      .authorizeExchange()
      .and()
      .addFilterAt(webFilter(), SecurityWebFiltersOrder.AUTHORIZATION)
      .authorizeExchange()
      .pathMatchers("/**").access(new JwtAuthorizationManager(tokenProvider))
      .anyExchange().authenticated();
    return http.build();
  }
}

因為是前后端分離項目,所以沒有常規的后端的登錄操作,把這些disable掉。

securityContextRepository是個用於在請求之間保留SecurityContext策略接口,實現類是WebSessionServerSecurityContextRepository(session存儲),還有就是NoOpServerSecurityContextRepository(用於無狀態應用),像我們JWT這種就用后者,不能用前者,應該我們是無狀態的應用,沒有主動clear的操作,會導致內存溢出等問題。

build()方法中會有一個初始化操作。

初始化操作就設置成了WebSessionServerSecurityContextRepository,我們就自己在SecurityWebFilterChain中設置成NoOpServerSecurityContextRepository。

接下來我們為了滿足自定義認證需求,我們自己配置一個AuthenticationWebFilter。

   public AuthenticationWebFilter webFilter() {
        AuthenticationWebFilter authenticationWebFilter = new AuthenticationWebFilter(new JWTReactiveAuthenticationManager(userCache, tokenProvider, coreUserApi));
        authenticationWebFilter.setServerAuthenticationConverter(new TokenAuthenticationConverter(guestList, tokenProvider));
        authenticationWebFilter.setRequiresAuthenticationMatcher(new NegatedServerWebExchangeMatcher(ServerWebExchangeMatchers.pathMatchers(excludedAuthPages)));
        authenticationWebFilter.setSecurityContextRepository(new NoOpServerSecurityContextAutoRepository(tokenProvider));
        return authenticationWebFilter;
    }

幾個特殊的類,稍微解釋下。

  • AuthenticationWebFilter

一個執行特定請求身份驗證的WebFilter,包含了一整套驗證的流程操作,具體上源碼看一眼基本能了解個大概。

	@Override
	public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
		return this.requiresAuthenticationMatcher.matches(exchange)
			.filter( matchResult -> matchResult.isMatch())
			.flatMap( matchResult -> this.authenticationConverter.convert(exchange))
			.switchIfEmpty(chain.filter(exchange).then(Mono.empty()))
			.flatMap( token -> authenticate(exchange, chain, token))
			.onErrorResume(AuthenticationException.class, e -> this.authenticationFailureHandler
					.onAuthenticationFailure(new WebFilterExchange(exchange, chain), e));
	}

	private Mono<Void> authenticate(ServerWebExchange exchange, WebFilterChain chain, Authentication token) {
		return this.authenticationManagerResolver.resolve(exchange)
			.flatMap(authenticationManager -> authenticationManager.authenticate(token))
			.switchIfEmpty(Mono.defer(() -> Mono.error(new IllegalStateException("No provider found for " + token.getClass()))))
			.flatMap(authentication -> onAuthenticationSuccess(authentication, new WebFilterExchange(exchange, chain)));
	}

	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)));
	}

  • ServerWebExchangeMatcher

    一個用來匹配URL用來驗證的接口,我代碼中用的是他的實現類NegatedServerWebExchangeMatcher,這個類就是指一些我設置的白名單的url就不要驗證了,他還有許多實現類,具體可以參見源碼,我這就不累述了。

  • ServerAuthenticationConverter

    一個用於從ServerWebExchange轉換為用於通過提供的org.springframework.security.authentication.ReactiveAuthenticationManager進行身份驗證的Authentication的策略。 如果結果為Mono.empty() ,則表明不進行任何身份驗證嘗試。我這邊自己實現了一個TokenAuthenticationConverter,主要功能就是通過JWT轉換成Authentication(UsernamePasswordAuthenticationToken)。

  • ReactiveAuthenticationManager

    對提供的Authentication進行身份驗證,基本上核心的驗證操作就在它提供的唯一方法authenticate里進行操作,根據conver那邊轉換過來的Authentication當參數進行具體的驗證操作,簡述如下:

    @Override
    public Mono<Authentication> authenticate(final Authentication authentication) {
        if (authentication.isAuthenticated()) {
            return Mono.just(authentication);
        }
        return Mono.just(authentication)
                .switchIfEmpty(Mono.defer(this::raiseBadCredentials))
                .cast(UsernamePasswordAuthenticationToken.class)
                .flatMap(this::authenticateToken)
                .publishOn(Schedulers.parallel())
                .onErrorResume(e -> raiseBadCredentials())
                .switchIfEmpty(Mono.defer(this::raiseBadCredentials))
                .map(u -> {
                    UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(authentication.getPrincipal(), authentication.getName(), Collections.EMPTY_LIST);
                    usernamePasswordAuthenticationToken.setDetails(u);
                    return usernamePasswordAuthenticationToken;
                });
    }
  • ServerSecurityContextRepository

    用於在請求之間保留SecurityContext,因為在登錄成功后我們是需要保存一個登錄的數據,用來后面的請求進行相關的操作。因為我們是無狀態的,所以其實NoOpServerSecurityContextRepository是能
    滿足我們的需求,我們不需要進行實際的save,但是load我們稍微要改造下,所以我實現了ServerSecurityContextRepository,仿照NoOpServerSecurityContextRepository,實現了一個自定義的Repository,為什么load我們要改造,就是因為雖然我們是無狀態的,但是實際上每次請求,我們依然要區分到底是誰,為了后面的權限驗證做准備,所以我們根據jwt可以生成一個SecurityContext放入ReactiveSecurityContextHolder。

public class NoOpServerSecurityContextAutoRepository
        implements ServerSecurityContextRepository {

    private TokenProvider tokenProvider;

    public NoOpServerSecurityContextAutoRepository(TokenProvider tokenProvider) {
        this.tokenProvider = tokenProvider;
    }

    public Mono<Void> save(ServerWebExchange exchange, SecurityContext context) {
        return Mono.empty();

    }

    public Mono<SecurityContext> load(ServerWebExchange exchange) {
        String token = exchange.getRequest().getHeaders().getFirst("Authorization");
        if (StrUtil.isNotBlank(token)) {
            SecurityContext securityContext = new SecurityContextImpl();
            securityContext.setAuthentication(new UsernamePasswordAuthenticationToken("password", token, Collections.EMPTY_LIST));
            return Mono.justOrEmpty(securityContext);
        } else {
            return Mono.empty();
        }
    }
}

權限驗證

權限驗證是在圖上配置的。大概的流程,可以看下面的截圖。

  • AuthorizationWebFilter

跟到里面,我們發現了最主要的就是這個AuthorizationWebFilter,用來做權限驗證的,然后我們在filter方法里面就看得很清楚了,他第一步就是拿的ReactiveSecurityContextHolder.getContext(),然后我們之前在ReactorContextWebFilter里的load操作就是從我們NoOpServerSecurityContextAutoRepository里塞到ReactiveSecurityContextHolder里,因為本質 來說SpringSecurity就是個filter集合,我們從ReactorContextWebFilter里load,然后在AuthorizationWebFilter取,這樣就能拿到Authentication來做權限驗證了。

  • ReactiveAuthorizationManager

反應式授權管理器接口,可以確定Authentication是否有權訪問特定對象。其實看源碼就很清楚了,就是根據Authentication來做具體的權限驗證。

代碼很清楚,就不細講了,我們主要是寫check方法。所以我這邊自已實現了一個JwtAuthorizationManager類用來做具體的check,內容我就不貼了,簡單來說就是拿Authentication里的內容去redis里查對應的菜單權限。

結語

上面就我實際項目中的一些點滴記錄,Spring-Security雖是一個博大精深的框架,細研究代碼,其實也能大致明白整體的思路,雖然webflux讓這一層代碼更加了一層迷霧,但是只要努力鑽研,總會有茅塞頓開的時候。

附上相關代碼,由於是生產項目,只能截取部分代碼,僅供參考。

部分代碼


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM