Spring-Gateway与Spring-Security在前后端分离项目中的实践

摘要:
前言互联网上似乎很少有Spring Security的操作信息,看起来像webflux。WebFlux中的ServerHttpSecurity与HttpSecurity提供的类似,但仅适用于WebFlux。SecurityContextRepository用于在请求之间保留SecurityContext策略接口。实现类是WebSessionServerSecurityContextRepository,以及NoOpServerSecurityContextStore。与JWT一样,我们使用后者而不是前者。我们应该是没有活动清除操作的无状态应用程序,这将导致内存溢出和其他问题。初始化操作设置为WebSessionServerSecurityContextRepository。我们可以在SecurityWebFilterChain中将其设置为NoOpServerSecurityContextRepository。

前言

网上貌似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()方法中会有一个初始化操作。

Spring-Gateway与Spring-Security在前后端分离项目中的实践第1张

初始化操作就设置成了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)));
	}

Spring-Gateway与Spring-Security在前后端分离项目中的实践第2张

  • 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。

Spring-Gateway与Spring-Security在前后端分离项目中的实践第3张

Spring-Gateway与Spring-Security在前后端分离项目中的实践第4张

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

权限验证

Spring-Gateway与Spring-Security在前后端分离项目中的实践第5张

权限验证是在图上配置的。大概的流程,可以看下面的截图。

Spring-Gateway与Spring-Security在前后端分离项目中的实践第6张

Spring-Gateway与Spring-Security在前后端分离项目中的实践第7张

  • AuthorizationWebFilter

跟到里面,我们发现了最主要的就是这个AuthorizationWebFilter,用来做权限验证的,然后我们在filter方法里面就看得很清楚了,他第一步就是拿的ReactiveSecurityContextHolder.getContext(),然后我们之前在ReactorContextWebFilter里的load操作就是从我们NoOpServerSecurityContextAutoRepository里塞到ReactiveSecurityContextHolder里,因为本质 来说SpringSecurity就是个filter集合,我们从ReactorContextWebFilter里load,然后在AuthorizationWebFilter取,这样就能拿到Authentication来做权限验证了。

Spring-Gateway与Spring-Security在前后端分离项目中的实践第8张

  • ReactiveAuthorizationManager

反应式授权管理器接口,可以确定Authentication是否有权访问特定对象。其实看源码就很清楚了,就是根据Authentication来做具体的权限验证。

Spring-Gateway与Spring-Security在前后端分离项目中的实践第9张

代码很清楚,就不细讲了,我们主要是写check方法。所以我这边自已实现了一个JwtAuthorizationManager类用来做具体的check,内容我就不贴了,简单来说就是拿Authentication里的内容去redis里查对应的菜单权限。

结语

上面就我实际项目中的一些点滴记录,Spring-Security虽是一个博大精深的框架,细研究代码,其实也能大致明白整体的思路,虽然webflux让这一层代码更加了一层迷雾,但是只要努力钻研,总会有茅塞顿开的时候。

附上相关代码,由于是生产项目,只能截取部分代码,仅供参考。

部分代码

免责声明:文章转载自《Spring-Gateway与Spring-Security在前后端分离项目中的实践》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇11.ThinkPHP分页IntelliJ IDEA 编译程序出现 非法字符 的 解决方法下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

[转]前后端分离开发模式下后端质量的保证 —— 单元测试

本文转自:http://www.cnblogs.com/jesse2013/p/magic-of-unittesting.html#3451709 概述   在今天, 前后端分离已经是首选的一个开发模式。这对于后端团队来说其实是一个好消息,减轻任务并且更专注。在测试方面,就更加依懒于单元测试对于API以及后端业务逻辑的较验。当然单元测试并非在前后端分离流...

基于NodeJS的全栈式开发

随着不同终端(Pad/Mobile/PC)的兴起,对开发人员的要求越来越高,纯浏览器端的响应式已经不能满足用户体验的高要求,我们往往需要针对不同的终端开发定制的版本。为了提升开发效率,前后端分离的需求越来越被重视,后端负责业务/数据接口,前端负责展现/交互逻辑,同一份数据接口,我们可以定制开发多个版本。 这个话题最近被讨论得比较多,阿里有些BU也在进行一些...

mock的使用及取消,node模仿本地请求:为了解决前后端分离,用户后台没写完接口的情况下

借鉴:https://www.jianshu.com/p/dd23a6547114 1、说到这里还有一种是配置node模拟本地请求 (1)node模拟本地请求: 补充一下 【1】首先在根目录下建一个data.json,用来存放一些返回数据,名字随便取好了 [2]在webpack.dev.conf.js文件里 在这个const portfinder...

SpringBootSecurity学习(20)前后端分离版之OAuth2.0刷新token

刷新token 前面的例子和配置都是从头开始申请授权码和令牌,现在来看一下如何根据获取令牌时,回参中的 refresh_token 来刷新令牌。现在在项目中配置的是内存模式的默认用户名密码,第一步先改成数据库查询的方式,具体过程参考前面的文章即可,来看security配置类: 然后修改授权服务配置类,在 endpoints 中配置userDetailsS...

SpringBootSecurity学习(19)前后端分离版之OAuth2.0 token的存储和管理

内存中存储token 我们来继续授权服务代码的下一个优化。现在授权服务中,token的存储是存储在内存中的,我们使用的是 InMemoryTokenStore : 图中的tokenStore方法支持很多种令牌的存储方式,来看一下: InMemoryTokenStore:这个版本的实现是被默认采用的,它可以完美的工作在单服务器上(即访问并发量压力不大的情...

前后端分离实践(一)

前言 最近这一段时间由于Nodejs的逐渐成熟和日趋稳定,越来越多的公司中的前端团队开始尝试使用Nodejs来练一下手,尝一尝鲜。 一般的做法都是将原本属于后端的一部分相对于业务不是很重要的功能迁移到Nodejs上面来,也有一些公司将NodeJS作为前后端分离的一个解决方案去施行。而像淘宝网这类的大型网站也很早的完成了前后端的分离,给我们这样的后来者提供了...