Loading

Spring Cloud Security OAuth2整合Spring Cloud Gateway网关

整合思路:

  • 授权服务器作为独立服务提供,用户登录请求通过网关转发到授权服务器
  • 网关除负载转发请求外,还提供令牌(身份)认证和鉴权服务。当客户端访问资源时,请求先达到网关,网关检查携带的令牌是否合法和有效以及判断是否有权限访问资源后,再进行请求转发

image-20220912235443138

授权服务器引用2.1配置即可,下面搭建网关服务:

1、 配置依赖

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>


<!-- gateway-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

Gateway是基于Spring WebFlux的响应式web框架,配置过滤器链需要自定义一个SecurityWebFilterChainBean对象,并使用@EnableWebFluxSecurity注解启用webflux环境下的security自动配置

2、 配置SecurityWebFilterChain

Spring cloud gateway是一个基于Spring Webflux响应式编程的网关服务,其底层运行时是Netty而不是Servlet;

Webflux环境下,Spring security同样是基于过滤器链实现安全校验,但如果要配置过滤器链需要实例化一个SecurityWebFilterChain对象,同时通过@EnableWebFluxSecurity注解启用Webflux环境下的security自动配置

@Configuration
@EnableWebFluxSecurity
public class BaseSecurityConfig {
    @Autowired
    private JwtAuthenticationManager jwtAuthenticationManager;
    @Bean
    public SecurityWebFilterChain springWebFilterChain(ServerHttpSecurity http) {
        AuthenticationWebFilter authenticationWebFilter = new AuthenticationWebFilter(jwtAuthenticationManager);
        authenticationWebFilter.setServerAuthenticationConverter(new ServerBearerTokenAuthenticationConverter());
        authenticationWebFilter.setRequiresAuthenticationMatcher(
                new NegatedServerWebExchangeMatcher(ServerWebExchangeMatchers.pathMatchers("/login"))
        );

        authenticationWebFilter.setAuthenticationFailureHandler(new JwtServerAuthenticationFailureHandler());

        http.authorizeExchange()
                .pathMatchers("/oauth/**", "/login").permitAll()
                .pathMatchers("/api/play").hasRole("admin")
                .anyExchange().access(new JwtAccessManager())
                .and()
                .exceptionHandling()
                .accessDeniedHandler(new JwtServerAccessDeniedHandler())
                .authenticationEntryPoint(new JwtServerAuthenticationEntryPoint())
                .and()
                .addFilterAt(authenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION);
        return http.build();
    }

    @Bean
    public JwtTokenStore jwtTokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setSigningKey("weixia");
        return jwtAccessTokenConverter;
    }
}
  • 通过ServerHttpSecurity对象配置JWT认证过滤器和动态鉴权访问管理器
  • authenticationWebFilter是一个JWT认证过滤器,依赖ReactiveAuthenticationManager,需要自己实现ReactiveAuthenticationManager接口定义认证逻辑
  • JwtAccessManager一个自定义鉴权管理器。使用内置的hasRole表达式鉴权,不需要自定义鉴权管理器
  • /oauth/**、/login直接放行,转发到授权服务器

3、配置认证管理器

3.1 AuthenticationWebFilter源码

AuthenticationWebFilter认证过滤器实现了WebFilter接口,认证逻辑定义在filter方法中:

public class AuthenticationWebFilter implements WebFilter {
    private final ReactiveAuthenticationManagerResolver<ServerWebExchange> authenticationManagerResolver;
    private ServerAuthenticationSuccessHandler authenticationSuccessHandler = new WebFilterChainServerAuthenticationSuccessHandler();
    private ServerAuthenticationConverter authenticationConverter = new ServerHttpBasicAuthenticationConverter();
    private ServerAuthenticationFailureHandler authenticationFailureHandler = new ServerAuthenticationEntryPointFailureHandler(new HttpBasicServerAuthenticationEntryPoint());
    private ServerSecurityContextRepository securityContextRepository = NoOpServerSecurityContextRepository.getInstance();
    private ServerWebExchangeMatcher requiresAuthenticationMatcher = ServerWebExchangeMatchers.anyExchange();

    public AuthenticationWebFilter(ReactiveAuthenticationManager authenticationManager) {
        Assert.notNull(authenticationManager, "authenticationManager cannot be null");
        this.authenticationManagerResolver = (request) -> {
            return Mono.just(authenticationManager);
        };
    }

    public AuthenticationWebFilter(ReactiveAuthenticationManagerResolver<ServerWebExchange> authenticationManagerResolver) {
        Assert.notNull(authenticationManagerResolver, "authenticationResolverManager cannot be null");
        this.authenticationManagerResolver = authenticationManagerResolver;
    }

    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        return this.requiresAuthenticationMatcher.matches(exchange).filter((matchResult) -> {
            return matchResult.isMatch();
        }).flatMap((matchResult) -> {
            return this.authenticationConverter.convert(exchange);
        }).switchIfEmpty(chain.filter(exchange).then(Mono.empty())).flatMap((token) -> {
            return this.authenticate(exchange, chain, token);
        }).onErrorResume(AuthenticationException.class, (e) -> {
            return 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) -> {
            return authenticationManager.authenticate(token);
        }).switchIfEmpty(Mono.defer(() -> {
            return Mono.error(new IllegalStateException("No provider found for " + token.getClass()));
        })).flatMap((authentication) -> {
            return this.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)));
    }
}

  1. 通过ServerWebExchangeMatcher判断当前请求是否要被该过滤器拦截,默认是拦截任何请求路径
  2. 通过ServerAuthenticationConverter提取请求中的认证信息,转换为未认证的AuthenticationToken对象。我们在配置中传入一个ServerBearerTokenAuthenticationConverter实例,它负责从请求中提取token并转换为BearerTokenAuthenticationToken对象;
  3. 调用ReactiveAuthenticationManager,对AuthenticationToken进行认证
  4. 如果认证成功,把认证后的Authentication放到SecurityContex上下文中;如果认证失败,ServerAuthenticationFailureHandler负责处理认证异常

3.2 实现ReactiveAuthenticationManager

@Component
public class JwtAuthenticationManager implements ReactiveAuthenticationManager {
    @Autowired
    private JwtTokenStore jwtTokenStore;

    @Override
    public Mono<Authentication> authenticate(Authentication authentication) {

        return Mono.justOrEmpty(authentication)
                .filter(token -> token instanceof BearerTokenAuthenticationToken)
                .cast(BearerTokenAuthenticationToken.class)
                .map(BearerTokenAuthenticationToken::getToken)
                .flatMap(accessToken -> {
                    
                    // 对token进行认证
                    OAuth2AccessToken oAuth2AccessToken = jwtTokenStore.readAccessToken(accessToken);
                    if (oAuth2AccessToken == null) {
                        return Mono.error(new AuthenticationException("无效的token!") {
                        });
                    } else if (oAuth2AccessToken.isExpired()) {
                        return Mono.error(new AuthenticationException("token已过期!") {
                        });
                    }
                    // 将token转换为Authentication
                    OAuth2Authentication oAuth2Authentication = jwtTokenStore.readAuthentication(oAuth2AccessToken);
                    if (oAuth2Authentication == null) {
                        return Mono.error(new AuthenticationException("无效的token!") {
                        });
                    } else {
                        return Mono.just(oAuth2Authentication);
                    }
                });
    }
}

3.3 配置AuthenticationWebFilter

@Configuration
@EnableWebFluxSecurity
public class BaseSecurityConfig {
    @Autowired
    private JwtAuthenticationManager jwtAuthenticationManager;
    @Bean
    public SecurityWebFilterChain springWebFilterChain(ServerHttpSecurity http) {
        AuthenticationWebFilter authenticationWebFilter = new AuthenticationWebFilter(jwtAuthenticationManager);
        authenticationWebFilter.setServerAuthenticationConverter(new ServerBearerTokenAuthenticationConverter());
        authenticationWebFilter.setRequiresAuthenticationMatcher(
                new NegatedServerWebExchangeMatcher(ServerWebExchangeMatchers.pathMatchers("/login"))
        );

        authenticationWebFilter.setAuthenticationFailureHandler(new JwtServerAuthenticationFailureHandler());

        http.authorizeExchange()
                .pathMatchers("/oauth/**", "/login").permitAll()
                .pathMatchers("/api/play").hasRole("admin")
                .anyExchange().access(new JwtAccessManager())
                .and()
                .exceptionHandling()
                .accessDeniedHandler(new JwtServerAccessDeniedHandler())
                .authenticationEntryPoint(new JwtServerAuthenticationEntryPoint())
                .and()
                .addFilterAt(authenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION);
        return http.build();
    }
}
  • authenticationWebFilter.setServerAuthenticationConverter配置怎么从请求中拿到token并转化为Authentication
  • authenticationWebFilter.setRequiresAuthenticationMatcher配置请求匹配器,默认是匹配任何请求路径;NegatedServerWebExchangeMatcher则是如果匹配请求路径则返回false,可以实现白名单效果(只要匹配配置的请求路径,则不拦截)
  • authenticationWebFilter.setAuthenticationFailureHandler配置认证失败处理器
  • ServerHttpSecurity.addFilterAt把认证过滤器配置到过滤器链中

4、自定义访问管理器

public class JwtAccessManager implements ReactiveAuthorizationManager<AuthorizationContext> {
    @Override
    public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {
        String url = authorizationContext.getExchange().getRequest().getPath().value();
        // 这步可替换为从数据库中读取当前请求需要的角色集合
        List<String> authorities = Arrays.asList("ROLE_user");

        return mono
            	// 只对未认证的token进行认证
                .filter(Authentication::isAuthenticated)
            	// 获取所需要的权限集合
                .flatMapIterable(Authentication::getAuthorities)
                .map(GrantedAuthority::getAuthority)
            	// 只要一个角色匹配则鉴权成功
                .any(authorities::contains)
                .map(AuthorizationDecision::new)
            	// 默认鉴权结果
                .defaultIfEmpty(new AuthorizationDecision(false));
    }
}

http.authorizeExchange().anyExchange().access(new JwtAccessManager())配置访问管理器

5、异常处理

5.1 认证失败异常

public class JwtServerAuthenticationFailureHandler implements ServerAuthenticationFailureHandler {
    private final ObjectMapper objectMapper = new ObjectMapper();
    @Override
    public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException e) {
        ServerHttpResponse response = webFilterExchange.getExchange().getResponse();
        String msg = e.getMessage();
        ResultVO resultVO = new ResultVO(ResultStatus.NO, msg, null);
        byte[] value = new byte[0];
        try {
            value = objectMapper.writeValueAsBytes(resultVO);
        } catch (JsonProcessingException jsonProcessingException) {
            jsonProcessingException.printStackTrace();
        }
        
        DataBuffer wrap = response.bufferFactory().wrap(value);
        return response.writeWith(Mono.just(wrap));
    }
}

5.2 已认证用户权限不足异常

public class JwtServerAccessDeniedHandler implements ServerAccessDeniedHandler {
    private final ObjectMapper objectMapper = new ObjectMapper();
    @Override
    public Mono<Void> handle(ServerWebExchange serverWebExchange, AccessDeniedException e) {
        ServerHttpResponse response = serverWebExchange.getResponse();
        String msg = e.getMessage();
        ResultVO resultVO = new ResultVO(ResultStatus.NO, msg, null);
        byte[] value = new byte[0];
        try {
            value = objectMapper.writeValueAsBytes(resultVO);
        } catch (JsonProcessingException jsonProcessingException) {
            jsonProcessingException.printStackTrace();
        }
        DataBuffer wrap = response.bufferFactory().wrap(value);
        return response.writeWith(Mono.just(wrap));
    }
}

5.3 未认证用户访问受限资源异常

public class JwtServerAuthenticationEntryPoint implements ServerAuthenticationEntryPoint {
    private final ObjectMapper objectMapper = new ObjectMapper();
    @Override
    public Mono<Void> commence(ServerWebExchange serverWebExchange, AuthenticationException e) {
        ServerHttpResponse response = serverWebExchange.getResponse();
        String msg = e.getMessage();
        ResultVO resultVO = new ResultVO(ResultStatus.NO, msg, null);
        byte[] value = new byte[0];
        try {
            value = objectMapper.writeValueAsBytes(resultVO);
        } catch (JsonProcessingException jsonProcessingException) {
            jsonProcessingException.printStackTrace();
        }
        DataBuffer wrap = response.bufferFactory().wrap(value);
        return response.writeWith(Mono.just(wrap));
    }
}

6、 网关配置

server:
  port: 8083

spring:
  application:
    name: gateway-server


  cloud:
    gateway:
      routes:
        - id: user-server
          uri: http://localhost:8082
          predicates:
            - Path=/api/**
          filters:
            - StripPrefix=1

        - id: auth-server
          uri: http://localhost:8080
          predicates:
            - Path=/oauth/**,/login
  • user-server是资源服务,所有以/api开头的请求都转发到资源服务;
  • auth-server是授权服务,所有以/oauth,/login转发到授权服务;
posted @ 2022-09-13 10:08  未夏  阅读(2173)  评论(0编辑  收藏  举报